From 29284f6eac1c752b6eccdad3165db45f5c001f98 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Sun, 1 Mar 2026 13:22:26 +0100 Subject: [PATCH 1/9] Add spring backend --- backend-spring/.gitignore | 2 + backend-spring/Dockerfile | 17 + backend-spring/build.gradle.kts | 37 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 46175 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + backend-spring/gradlew | 248 ++++ backend-spring/gradlew.bat | 93 ++ backend-spring/settings.gradle.kts | 1 + .../AllowancePlannerApplication.java | 13 + .../controller/ApiController.java | 506 +++++++ .../controller/WebController.java | 173 +++ .../dto/AddAllowanceAmountRequest.java | 5 + .../allowanceplanner/dto/AllowanceDto.java | 8 + .../dto/BulkUpdateAllowanceRequest.java | 5 + .../dto/CreateAllowanceRequest.java | 5 + .../dto/CreateTaskRequest.java | 11 + .../allowanceplanner/dto/ErrorResponse.java | 5 + .../allowanceplanner/dto/HistoryDto.java | 7 + .../allowanceplanner/dto/IdResponse.java | 5 + .../allowanceplanner/dto/MessageResponse.java | 5 + .../dto/PostHistoryRequest.java | 5 + .../allowanceplanner/dto/TaskDto.java | 8 + .../allowanceplanner/dto/TransferRequest.java | 5 + .../dto/UpdateAllowanceRequest.java | 5 + .../allowanceplanner/dto/UserDto.java | 5 + .../dto/UserWithAllowanceDto.java | 5 + .../allowanceplanner/entity/Allowance.java | 99 ++ .../allowanceplanner/entity/History.java | 73 + .../allowanceplanner/entity/Task.java | 97 ++ .../allowanceplanner/entity/User.java | 61 + .../repository/AllowanceRepository.java | 27 + .../repository/HistoryRepository.java | 13 + .../repository/TaskRepository.java | 16 + .../repository/UserRepository.java | 13 + .../service/AllowanceService.java | 298 ++++ .../allowanceplanner/service/TaskService.java | 132 ++ .../service/TransferService.java | 102 ++ .../allowanceplanner/service/UserService.java | 42 + .../allowanceplanner/util/ColourUtil.java | 42 + .../src/main/resources/application.properties | 12 + .../resources/db/migration/V1__initial.sql | 42 + .../src/main/resources/templates/index.html | 122 ++ .../seeseepuff/allowanceplanner/ApiTest.java | 1242 +++++++++++++++++ .../allowanceplanner/ColourUtilTest.java | 33 + 44 files changed, 3652 insertions(+) create mode 100644 backend-spring/.gitignore create mode 100644 backend-spring/Dockerfile create mode 100644 backend-spring/build.gradle.kts create mode 100644 backend-spring/gradle/wrapper/gradle-wrapper.jar create mode 100644 backend-spring/gradle/wrapper/gradle-wrapper.properties create mode 100755 backend-spring/gradlew create mode 100644 backend-spring/gradlew.bat create mode 100644 backend-spring/settings.gradle.kts create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/AllowancePlannerApplication.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/WebController.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AddAllowanceAmountRequest.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AllowanceDto.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/BulkUpdateAllowanceRequest.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateAllowanceRequest.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateTaskRequest.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/ErrorResponse.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/HistoryDto.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/IdResponse.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MessageResponse.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/PostHistoryRequest.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TaskDto.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TransferRequest.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UpdateAllowanceRequest.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserDto.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserWithAllowanceDto.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/AllowanceRepository.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/HistoryRepository.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/TaskRepository.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/UserRepository.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/AllowanceService.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TaskService.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TransferService.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/UserService.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/util/ColourUtil.java create mode 100644 backend-spring/src/main/resources/application.properties create mode 100644 backend-spring/src/main/resources/db/migration/V1__initial.sql create mode 100644 backend-spring/src/main/resources/templates/index.html create mode 100644 backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ApiTest.java create mode 100644 backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ColourUtilTest.java diff --git a/backend-spring/.gitignore b/backend-spring/.gitignore new file mode 100644 index 0000000..67bcc2f --- /dev/null +++ b/backend-spring/.gitignore @@ -0,0 +1,2 @@ +.gradle/ +build/ diff --git a/backend-spring/Dockerfile b/backend-spring/Dockerfile new file mode 100644 index 0000000..c040705 --- /dev/null +++ b/backend-spring/Dockerfile @@ -0,0 +1,17 @@ +FROM eclipse-temurin:25-jdk-alpine AS build + +WORKDIR /app +COPY gradle ./gradle +COPY gradlew build.gradle.kts settings.gradle.kts ./ +RUN ./gradlew dependencies --no-daemon + +COPY src ./src +RUN ./gradlew bootJar --no-daemon -x test + +FROM eclipse-temurin:25-jre-alpine + +WORKDIR /app +COPY --from=build /app/build/libs/*.jar app.jar + +EXPOSE 8080 +CMD ["java", "-jar", "app.jar"] diff --git a/backend-spring/build.gradle.kts b/backend-spring/build.gradle.kts new file mode 100644 index 0000000..069add2 --- /dev/null +++ b/backend-spring/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + java + id("org.springframework.boot") version "4.0.3" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "be.seeseepuff" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-thymeleaf") + implementation("org.springframework.boot:spring-boot-starter-flyway") + implementation("org.flywaydb:flyway-database-postgresql") + runtimeOnly("org.postgresql:postgresql") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.boot:spring-boot-testcontainers") + testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.3") + testImplementation("org.testcontainers:testcontainers-postgresql:2.0.3") + testImplementation("io.rest-assured:rest-assured:6.0.0") +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/backend-spring/gradle/wrapper/gradle-wrapper.jar b/backend-spring/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..61285a659d17295f1de7c53e24fdf13ad755c379 GIT binary patch literal 46175 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

pVX@HRij*jw$g4?}Pn zE7RuaO3V!l_a{`|jsZVjZSR#tYwAffrvo3AAynZ^vzgSR#N_HZ6Ark)t{_hJ^zSa( zT@R*X#7rxlaj%ZVUZ1?7!Q9{bw(p9N;v)bZUqGgPC=O&mM zRy{1k%Hlr=aPWCif%s7!4cpn_cTyB1=#k?e8m}0C$)+&PD!&)F?>9;L&0Lpv)ZfP| zJxlb;PjKA4x^1R%?vIk=kv;C0Y*;|7*_mO)hTMlfPH5JcHa>0BR$wlt@&-wZufD82 z51*ufTeW5&M!0=a$FS@0MJRlk*~l8^Wl?2mzt}H8ae}hQ7tSz0sBJs+8lQ!`o(21B z@HNyMoH{;2l$8FopO-a)0DQ&f_jq)|ZPO}_AjDPtuOl4>R^0rLnok(Ezuu@$4lJ`w zQ6-4DQIk{FwQJspTlz!>L$CVj^cN<|)t^;jR~M^L^a=dr5aA!{qg3Ek9p;X{QRIg1 z1oE`2L#=6s6vh%=R(TI9Z5ReZy&?Jtj8aEcyCiP*YaYk5=!QbxQSz|aBk58{{@nCc zSY}$niG-_Uad_iRV56Ju8STIoe{*WWn3_?3>0V>z8)z@g_|dm5vKgxu`{>`)X}aw) zyd~I|(HFpmTO&3smRUnoB$VU&snAXEY(aq=te76JpanOdrwx}UD4D8MQ34z&zcD8z><`W?<_; zvO01*U(i7v7=EAJ@&YE- z4Cz5FWI`J^+_;Ez1p&jMET;4j<<0ymV(~ma*ooWab$s6DuWt>sP0$fuap>j|b@rOb zu^i4yE`d@_H>;F8*y;JfvhSY_o*1uZB+)0G+l{2nmbRR>POBwArWP}e z*`!BSjr`p73wW@iA~}h|mFJDOdP|bAlqD)jwN_vU{ z0ntkb0iphH{UY}N?H5%fR25`pw6s}OWdGYUvdqjNg|VZ<>;{luC*iGup0bRpG-1*u zLmD>P9mq$M!k->%T2{@Ea^ZR|8LZp2lzpBQFAfvFIUps_-Vxkm4ldisDdti7Bn(qo zAYco0<;Bu1tt6?z=(H_4yD~5qL+2##Hfo|6qRB-vFmQ}Xpo&Qc^GdrM6&iQtrIVT_ z6q)qyz^vmNwsqEnS6Vw6kZ1XSL;dx94s%n6>F=ht<9+@6=i_*PK35N0Hd_yKD<^9< zODB6aDOYD_a~CURdlzd74_j|%YZosWKTB&jFMC%PR!b*yPtX5;conr7MQ9H6g65XG z7EMw%FD|O_`*U$^ye1(o}oGT&v6r7mQ)iC|9t;%`Wt_`W`dAAT;#O+)Ge! zPY6Umf)7Er6YsZ!=pEz^$%f~wDcEbz?9OR@jjSa(Rvr03@mNYZ%uLF}1I$B4Hj~*g zWOL7pdu2IQtK=^>^gM(G`DhbFDLZd6_AD4bHKi+I<{kGj!ftcccz}667=-{}7`0~m z(VVjxK=8g9faw}91J}cSq7PrpJi3tMmm)~lowHDOUZfP++x{^vOUJjZXkhn7qE^N! zV)eH6A;SGx&6U&c1EFgS6CAwUqS$$N)odq!@3|yVs}Lv@HEcBe?UTqFr9Nyab-F_) zNOXxFGKa2*Z|&o&`_h+{qBoSkb^_~=yo&NYU~qe1|9&TE|8^(T{$GE;wbq8_qB^!o zWNUaUctH}Q+oBtk0YrkWOS_G@9aP2`<7DUWB~FndluuPn;S@}GiG2Iia25p++<(6C zea7mI68gN(*_{_OvF&*I?P;Q+ZzmWcYlw2__v`ENA>SnKs!v266LL&z9X9riJ-15i z?+VKr6gj*!-w2v^x)aO%fNEX5_4-u@zsW(~Hen6*9N_w{$})i6E2y4Z$h5?;ZS!i! z#Q>M4TTsuI9=p|iU9!ExS=~piozz{USJ)(nwWf1TYy0Ul2epIh)bcRZA|?PU!4VrJ z^E`vzA;ZAfgAm2#Tu0K-8E!~1iW6{oBl4lS-5Fc2%_saw>BKrIuW`^4za9w7veO)+ z)~?rp*f&V-xoXD~e%a9Df~ixzE@AMs{a8am6R+SXhXPfqv!>(-9^g7!X;m~14_ReuNF;J z{)~ysZBHLY*>ow*`^ie7bhc3H$N1qVxaGt6xFusWF%owkNrl|{nn?h~fjxFur;u%{ zPf10%f#iPYY|=!*HH!WbI~jskWo9 z%vV&6J9*nXeR4B9>xWboSk9Eo;%Rc=iE)t~UQbj~kZ}4=;KwNN^|%wM#RG(8q5C1k z>f6|ABKw4TzF_F&4eI{KI~)AqlIA;D%ZP^dwp;M?kIJM*Nn1jZu`KDt@GR-|U9|cI z1nW&P8r5WLE6a}#e-Ogslihm9#r{J2n@QFmcUAr#tQi)Hpw4ELC$U8t>j~4TVQMBeq1ZPK`deHgU!QY`%5H8F{fX}O}fV)= zw|oE_A51>pxJ5Kp`wcemi6jERtbEsty7FV`lJt6lR?dhxnyg>(GW9ZID_9Ii$2i#G zdN8@uX$m?D%-Eq1v57~V)v%f8Se#&b=gLhg@U ze$?D?oYb{i2w@tccty}{bKwjeaiTuuL?Y(;;{c#-8v&4O?%RgKiToLey0P8POL9Kwj|;h#ul~;=V1gq!oLVrP zlwx-xwyB=#A|5Bw>09TQ+~jkdmGnJ$YrZ%|h0VcBeiw@b^J+BlumSY_)*u&%R)>JW z7(0lRtg+C9u68--7Kw&9^AeL`o5cpi$Cy>&&kBT$@!Nt_@iuYI<_q4`b~7LsTn<38 z@q_=pRRz<8vLEbi`ICI> ztVoyd+|~B7*q`1YG&7_fPT`QJ3v;k-%itr5x!$sYj;Y?a>MMPep@UxVTF#+1EV!N> z_6H2hN=N0Xcd@IV%9NJvYR74G?Ru3xuB)BwZmD7Zq}qomtW}na^#(qbREUPzmYN6p ziyU)gFriO8NCoWQj0cX0evy`_iBWmXRAqjv1s zUZv#j5;NRuz6K0Q1#jyMzmijh*97>D-0HyQpPUWas$-Ay(?|{416{@{5KP2ka?PEc zP8oI%1X4Fzj3>}EjfCUk#(+zT!v(}iw3p$!^Q@S^2sG(pZFxXmvZD}i1S#$t^890< z{qTT~_hK@t_;8eCDm(0+KRWb6`iW#<@oqli&F&)ud!?o@d#&sm5DU${T#J~}D*(W+tb(BT9{p5*$hl>S5#Xso0)3^_UA8`Gf}moKyx7WW&Za0bEVdTef`-Tw?^P zr({3nnvcOQnn@C^v4ZlJ=yE#rD^h{bm(KZBy#fUGpq~?g>prt}JS^tFeS?=|m?BaE zJ@8ZH<}v0~>8VyqJvJ#}R!cY&OHr9QC&Le-`&+%tpxZJGbNA}s(-?PsV!b$q%&_0+ zC$k1nfCE(B(j~5wJeTrsc466K?t9o4ZikU!~82D-nTxfSLC5X_z)Z!-7`Mxl(>;hU& zwS|rLUmoy3J@!cI)A2T1H2*w45C!(c8--k%iCVGPe+S%NbpuMfDLuXR2R<(-Sw*)Q7->L{-s5w3mfX% z?>dwU|98h&rogmI~+Qsg&`Cy24+@ zI~yTIuWMrcD~v&N)2vQrT9SR!dG`fB?z&e!-|lV$LSR7AG(bHzQ_;o8Ks!klRZlHs z@5q$YVtIP|a<0ze&Q5FD#f;Ht7tgR7)XE`-e2 z5vVHX7yNJH@VDzGGCwD3&Cv(4HA~0rre@MyJY3FgVyd_{ea3O;yVeEQJ4*-)5qs33 zN70F!zWStyRS@NYDW+6gDxGw=`~nt08}PMWhCD6!_JVcmsBLH{IV-gSc^LgclTkID z#*&}F&%i9%MP&SES zMzGEc)ZNPy=Pe~PxMIJEGf}r)daA7PevJ z9~2FSl=99aB`|MZDS^cR*40E>X4EU#m6FHPsurfX_nA42aR38WBr`!09eh=CTMTU4 zl~%%^;KR5%NlSXF?X@|}Nzv4dcNN+y5A)(8=UF7z_hF-i$MKDqj$UVS0g-WPyV6OL zuL{5wAthWbw>!-gJc}jYTscv0L})-yP{rUPfv+k9P(53RgvQc{t83(%8=TWEnJ)wh!#>`}qP_=0d( zpXBD5ujnfd8S4dSaF&g4qmxD%ZcDIqHsbGQdogW$0;r7pe{%LxZvJL` z)Sw{e>}9oM@k=(Jszzv1@-s+_s(2(wE3G)fjDXHCM`v_@jV67e?bV5N-QD0$C3zKK z-N)guBD&o&G#=>Pdw8OLjXj44&;h>!YZkRl>@noB4|)5}Ii9GhIkpa4&kWOcOhyRr zYx5XE6Z?9%mXL=$4#3A_%wWajqR1kAHqKxmm$x5@7@e3hWo_MNdf6MM9_$VgpoL*$ z(q{CFrM2<>{&S6Y`Toe=szf)7`jYyq-w&el6W+@arE9)tXY|B9U+jR~$~pq1W1&4( zf1+!D9CG<}H;#`2V#UaNc~{l_5Ivd<$=ro0i`rjH&%*uOT(BN-<|^pgFE!NF@KU5* zj~NZ;r9SIE?q%=3o+iJq==Y@ncGrYy%J1c~_suJ-ISHZ8;}7Ze!05^VW#JnSZ{I*& zIh*vqjYFYI!RPlGne6eHPoDm#*a$UbxXeR}t=rDi%u@AYv^@enQ$TaphrriwAw^mOF=o zL4X{Io~71KNrW8qCZt1ZAB`G432Db(WnJIQ9Xk;|poyayjFsO+K(=F|m6yMLxTfq2 zhmA&U#r#NiiRz~z8p#Dq)Z<0#?5fl-h3c zk>UdIdslOZew?=b_};J6j3dtba-*VcI`qcbk;`^8>kFo9S}}Tt9TLu=Z1ztD2YHPu zSZgnhwj72$6Yfmz|3b25Ha>8oD1+a}*z1w7`#@Py95vVcvT9dWRWBso7}3^OX!<5J zFcKmCk8_mJw*DB@`1;2cs z{yw*z5cIMwIsSwBJT&y%JBO71bq8VD$xeovL@et#f6tiC#UiA3`K|1TtQDghPWN8P zEdjNjpM*NYM&Wyck2a`6H)|X}!r?3)uN- zo_>B9W*}-{yshhLL1%rV{8BzHnQYJXCX7}POY9l?MPqbvfq+{Hef^*yK&|jtpz=8H z_xgmW~dlvT_#3qXgYW<(+du)1J=XdbY5|3?mgBC!dit@|i1pYvZ=t));Ws^GhP?7etFJ#A8#?jg99r^mOhBAF0jXRypO-&E7a&sa$~AcYYwYm|HmNboB84e)(T zMbK`=mwl{EXTkYc^^u;wdYm$I2%i?8R^+Xf1%XhS$iBcj=n`dTA0<<%tBGKw#pH_< z7yYlWMvJ8ygFM>pK6F^?P(R_40w80B#^gTpEC+Vb&&-!6^q&-vYPz)}``@sQ%YNR_ zNOaXl*@?QG{lR#3Gsel}$Q`3G)^I1q+oN;@z?#FkR0;YMyIDh(oqHLUT< zk%gnOLPl=j+HtG?g_Bx{A*S_^p$TG^ut?Hm$v?F`vMkXn_0D5fYW{-H;0MI!vWi7E zW&b|5>`<5JSg1K8FkRW`QJo!YzAX9xSr!^0mZUEfk+e_~Hmy%77CP-~XCFy_R*4Ny_`rntN5nAV}SQ6N8Kqw_8j7b%7ZDR?e^>X8K<8bXzAdC{U zbZE%9m#;pqPn(rbEIJk19@n!JN~SaxS$`yFfwM#h&6bLdZ|{BnweivPwU}5iB>tH2 z(DDBM^0Zt_|Dy<)@T|GowT3~5P4IWdOi;~Y6(Z-Ao7$ppc<*sKv0DE2 zQ7fJ1S??EtK+|tfC`0&UMEUqs_0z_`Tr-_=AzULJshV->?K>ppr+5%W&=*Se!)<}1 zK+gBXZb=Qr43OMnp>Vd>VvP)(DB)hLH~_LNbUK&g#Uu=wSZ1f)8T(5(=Gf2ks`Qa{xr90g&RZXd!6JA1Aw zH~bvvn5N$5qQCvfR*XVJ6iySM_p3Q6jj2|AA&s@!J8y>W`{M#gi1*@29nCFLvMWUb5-6g;Dkqe-W%-k<t{j$y~ zZ7Jv-AR3~g)EWPXi8B5gmP=?)iT9XMa^Qn@Af zcoYxd6o}pTBdGwc$_4n>X5-}pENro_;kLbQq#Dhu>sziG^)7u&Xr2tw>{M4F<>)%h z*d@4(v_5g`Ak*QtHlqz^vB9PvwxsxB4q`LjQ9BXRa9v*#!u0RuEzlJ)ycVg!jAzM< zYV{~*@!zH&U&Ky~T$-R{;HFjsr=cfwi1SeDIht|kx#-D|XfF8RB4qEs!reEjM<8hv zU=xYuWa`j&_=@NplwLBteU%fmX+IHI4fhNhJ(9zDJt6~n@mvvoH+3AG!+P>6J zoG)X6Iw7fjttAl^B_}-c(@4+*+h?Ha7Qe8QVJ}i!j`ualoyv4$& zTM5iU^f(^;K#s+&Qy=p_&aT6e@joE3-5OeTOqCbNH~Pmb+&wu*+Uz_5&+87~+0ARQ z-azQa1RfyT*cjWoYYQtMYJ{x=QO^7#VGg+K^X1L>lgQSiibOYd!ftWVlqi~aDO=o- z+b(cjHc_b9&hB%0moVs3e~5e42#vIrUbmI)E&zIrg7U)iRg@&c_Im;P!V|MaVmROn z?(JpEilGtTNb(aa@@UfeGqinFWh)iFm#LwOlE)&3%1~3TQSZ6O+$L@Lu`y7R^%~B7 zE}woyC&?yDU{|jD)NRh;$_FhR(|uJmsygG?T>{I2e56P`okogpWz{AU=73=yy67$ zcC?$q5B2xzV+^K8>>@tTcR2t~S#l77fpjIs0i$7=-9#ZS6mO&XpEqzg&DE)guyYm} zBoC;IEiNnv+0Qh}gVI%z<>#T09$#O%uyxfmobpOu2;?=Z-aZz6=B6kz5tC@rCfGX) zm<}1)3w~Ak;sJLFb4YQ8qVXCvDPZy^^(`&U1ynG$w4j!T$Pp2^f@mf0->j*ie}?xL z7WKMq_bK0TX!EyC5YGREoBl@HlmF3q9iv-mHLP2?PR$&VVlu(2lhn8^qDPP!iGg?h zzIDo*qoU|zggy^{%OZ?O8VEtAn78x`78Z~9{lSORlH*gcFFj!%J4HSZEP6Hzx`^H{LQLn>9BZE|(h!O@#5EOOBZcF z6-BayPVRUt0FB1~Gxql91k3tCxa8S(1yF5Zj?JXj^bmd60?)O(ng`Cu$~PW3dr}X8 zN0(%@SE59PaYtS_2R@rPDH1?-YAk&U%Bs#Z=4V}EIOnPTm}=;NWXJ80W5v^rP&yNw zOx@d(3Cb6uuitL3y+uFwv9=7EN!DQ1^%`EH2`&8D?HfvbAJ)#-iI= zlk*%1isoKmj-Lz`F!S+fW>x2w%1EB67abZ-T~^X9AReExl7sV@p9J8-1MZ>)VHZIm z?34yV$eyp&Kd(_of|WxGRb7B97~_HOR0NM;!K-gm@lH*%e@jhb{|Ov)Tpa(CBr;v= zQWZ-BT_m#=dlD(b6$e{ysnx3s0iOvUi<*Owh`j_qD!OBrQgpybQ~6jcbMp(ZWJK7{;R~r`CMiT z=_TjMgTlunNtE_VbG3eEqBqYns zV(n9T5S)pHyxSo=K-cG|D4z%`iKj@6P=$8kBid9^p^eMkn)3_HY4ENhpZ_?y#~&^q zTK>Z47dR=-AKZP##bkI~@>DexVZ9&9*vlk_BG!oJL1Ei#M3yJM(huR0QN0~M65s`i#`o=sciY?Ti;BPs;rIZ*Nq zOLVct7)Utdh%@Wu>TOw>M#Qu?*$o%i<8yo3KN|t0Y>nlq@cvM>s=!?CtyXsp#$?kii@j51YSaSHmqcD8K`ZPt{xYoH2h@X=f^)X&z zFqmL5sjK4cP8)@&nR2(wmzuA-zqIjoejdoZgD@i7SZ=glz76thfPhX~?i}^91xVVqU=pyesPK|Ax?EHnf z1O&K~Eu-T7cXLWl?UmAoE&TI@5*p(q*457~$mxu0e ze`?(Db8+hu9<5=8UiJ0_XK>hNA3^o12oCJ9D3=tOW);qG~lGfzo**>Xb&J}^Sz2Xu@*zcJSZM$@pHRhL$(%F)^$XaQro=Z}n;Ggf(0%SH%kli*5S`#7~u z*M<7&V*x48gsm0 zVUA_fXxXOx(k@c{oqGAp@b;izt}*_E2Yg|KJCV#CU6bcBo;72f!e%Kp2cO{V?3Fe; z>*8^i3-tkB7afkzC=wr4lTZ7o zsztT)HP5h$sNA@YlZtsRl=e&#Gl(QCszU{lpV(7~#vo^tR@oKk+x_vA>{9osLFsoy zS5)cL5glpM(sKT?8kN0^6 zqO7i<4UJYoF+rGw z)XET!cC!7sc9=ADGaCx}ewNH2F=eNn6mB&U6ll_bUDLk`21UpO#-y7->yTKIaI zZ~FG@O%6h9oJ%<1*TaXGsoji}?}tFbJVcwX1M=*aN60z#{5kg0_Z5>0uI~9vyp@R? zF(fli_tW(z(;EZXwIv(En9K(yAIs5~r2#tmIeG283az@`SA{HRf(#eVG=i!Po8$Iy z#~C&U@?B#rxgN=)qPzmQiPeE@&*|`S5~|rUOhc~rg0=`*x~v)Buyu}`;_64P7&B&; zX}AjY06Y@6)a?YSm-GRO%6f6ePC<^5w#0~Z_^LUu8VNnm)Q3^EfJ!W!p_0zgloie21K}^yuphA{ zr#G-tJ(dn|L()_VxUEim`lAM%-uW*Go?6X}k%Et&h0-V;ux`rvnYSm0U3mpf# z+auH5I<7}3GpsB~X9ldCt!$yBe5gUfraC6~=t%kSWLP(~_J=rU7 zR0Q{HWo|me08i&@@E?wZ^*zdJ45^LAG8Q_~NJ{>u5p<^$TyN3Jlg9x4;5;yoq*mdt znlDg8QcrIE?D?N2zrl!;+>Y>FoKcq~I;7>68J(W(V~*7VJ8M>A7|^ zP{=lk!0_Pc{oOSi0(6+_oJ9L%mJ~cV#qP_l8Vt2^s(wW|U9d@L5YO|Dx&W(SYB6TU zVvSt;VL?E|24F%SW$}4LUc`Ej;2X*s~%}Zs}ENa;}C`S-lWhTf07(0-sp+ntHd% zLgeH>7(T&*a9hy2z`|}sD;WmXD(L#Ye@teC#@?WZzZ0D1-x3`2|8_+Gi{Sp5)%*+1 zIjc`84vAxnSUN7Q{Hj{6i)EG`!EZ(?k0FQU!(~L0%v?O+CCR6@re%maiG0RmEi2lE zf7aM@9>~v~`Z&|Ub^m&Q3%iR?1l7RC##cw@OCAQVDA{%iC*`|?vfx+SJguGM=T3-u z4&+u)a!M$B48?#&<4vsFAXRj>-yxCvz&uuv;~frmzdtFPFj)L0BsSe*Gmuc`JD!#z zPa`c$gHeOUnc>^CEoevD+?_;w1|J|%L z0*cBks6lMxj!yTto>uK;kL4>$Rwc49p87NFU#fJO*KMo$Zewfzc8K|35;l96_aROf zb0;<%`}g5;b#pH}Z4YxFYY$IzCn-B?OGj&uf7v^4ohe@|9sECA73_=L5t!SW<_J&} zGg9=4nxsgO+&Q?^;wai+ACFW({&aY@f|5)>U$2{*-o+YYL29T-j8bB!`?2O6xB*mp z+m+gyhKbikZ(C3UnQv?1h^n0mCoT zG-)F7l#@A`)%bDwv}82PRoxo`N5Pnpx%LXG{7CBroox5+1)Lo^iuuGn%wB2(nvydI ztf;oYgnZ&zj>dZcMJ8SZ48a}_QZq|V&|c;}^%S&F0gedlP8tIO2R$<l0~Y0BWA( zSV|vwDB)Es1cO6Dq94jGL!#akBeCo}wGTYxbkfJ?HaSvNHU5IAga=PON?4nYe?HDt zz9--xcJ4mr8Hv&`-Pnm^es?x-zu-vqF}@0PQrw$uUTGzZBaPo_tZ|6?!%1$GddLfb z&CC(L)r?4F1VbnFJS~-H-m6mvRWiyVG7iI1-yhTnxW4%V62OxrjwT1wPAq-1?xeY3 zu97J`a#Uz!v#4y|8fjcuT@@ZuCUGYg&E_#?+;;)qd`m!jTA)%IOpQ?9;F-FQO+qXt z`z_Rj1`W8JS5BQCAb;9L#~CR4kV2p@K8BW=osN~CdGpmvj1%vXp(m8PJO<8E-uO|H zKjAQ+ABcrLNeMYreKI)BLzK*JDkHnzBMT7j%B~n`y*HS(P#=B2&2l4Yt`TF4VLhS- zM)_I2ct`%#d7>=lTbk<`4dD_xu)G)9RkK(@s;*&S^S251p!_$ZZHu)B7$M7?lHr-W zF%kEdYSwBGCi?dAMjwuuQl25^@qvB7`K+O3hKRZSSMK$|L=-#52Xfh0(%of7Slg56 z){|NTc7J~inp2I8F?ICJGS>rwP`NzKI!b0&NV!ysj-Z+@6E5SKuOjh|9@9KmC)Sq6 zc2*b44y~m+U);H434xpz7!4(t+WhIxA+fx@Aj-?SGo2BfY$dv=n1dS9rJ3*GA|GM7 zEsHJ%0?m=(MMtZJM`;;ImPA#DeXRr&oCH3CK^`x-Th#6RZ%;(*j_1a+w{&)aShu7r{tdXdk?WJ-bapM0|s?&8F+kibcI;Z z9Z-UtlJw?oG&;&NZSB9IEi;x5-qJKjWQrGy5d$ARAQ$wA@+G`d4m>e;Mm1sNfBDuX z;AlPXi|TGm(BpnE8T-ZXf{W~0Wx0qQ923F!n=H|$ktTp_<36%e?#jZTR%lsE?s`|G z_T*G`Yot#9M-G?e$E8&Z4^~CZQy!|3PN*F zDNfkD=^5SkBe6Yl_Le?z-ds^Xu zUGK3)J3ER-q{i5xeH_LQ#opHd`kzkZ8OR$wXuGOI0S9!4$bxd9rX#XpZE1rr4^nlI z%#Ifniqpe2QUU|_*1hla_WJzF5>$w}YuHz!Bn7$|L3T1o(*;+m?~4zM+b*Rf`2F@C zFENS_$mw8?Q|%@8ZDthiuM{w~NTxxb&VSsRle7&MYMAtnOu9n!RY4X8?EYiSeikH9 zOZndU(*0WjmH3|m`aikY$<@;Fy}`luezV8P+tc3XeMs5KTEf!O+S60T+{N7Xe=)PQ zhKd@t1bWcS73alQs#@~xV;CYJB5Mi?KBm+I_4{>vPgk`|r*9%;rv=}|<6hAJe6m%Q zMI{z_E?vq&91RPqy7IqXu2FoPGxhxefqJ98J2f-&`?k`IayjoSKR?nE_Zo_J0q**^ z=CMK65eJ9MM3UF=fpVw%jQosAdgrbkV|?jWk^G=GZgIWH-m}@m#m}e~pO>~^LxQ1C zxf5=MT9cUh7zX(?ajfHlS0m4UuFZU?mWD8edgL(v#~-b6dRBli37)yq(dkXa^0qYJ zm2>PSwXHmOY->)I(>c=@V=H#cH4iqkr>!Jcq>Rj7HCe5!sF`+DSryVrGhj1JPn0w1 zpz1F3V?}jAmjhC2W=WIhi1|62^IeKs_Vuu>tvlSbf{BEZssNH}YC!RXPf5va8 z&*O3h@9IqZw?VV$|3rnim%S6)e?vph!`#iy+C$pj^S%9L@&1{si;jnrl&j0TX1^=> zzle3jf3?G?B1XQFBaK`)JeJ#K>clF%=Vunm%H)`gIijk*u5HkZTQe8UY_h>oeW8^p z@_RMWVv0Q*F@)Uisoy6=JZF1;Y-Ts?hz7wmqN?rggTXHQJ*&xJNSfp}aD++2QG~si zmZ4!fZLnB;l)F@pm1^KxY6sa9z3@2v>*mIZV!qbQltmvKmnn`wiCxdz|KaPMqC?x7 zcHP*vZQGc!ZQHh!8QZpP8#A^sW7~FevVL5gZ|}V>M(b@{_p08j-tp8sUL>;HOB^b$ z;hIbdt|h(^Lz4!n2$`tDF>w>d+R^r-o8L4CV$Dx{(t;5vTIc;CPmAYCX2oT221P|P z0{m6DMhT zWW~*jfZ!{&jQk}73p}09Tf0mmdonALDG0GIE_*DY+Wdy$#(|jSR0=Mb{Usmq-&*Ok zCsP?iLH+L;SJ7sgXGBvgEBzL9X!Z;RdYm;+&8*;3+WY7|s0-y?RN9E6UFwIYEl&bu=-nMHo)d+Jw_>@v)eZkY$8$E+&w}~w$k+G*`#;JKQIBmWvt^#A{Oa{KQHq8GHYbN&e;1A7?*3)>&I>Ywl-Vf>E( zvQe0@{Tbw`B8+7nj^iMN)JBJMJ$R(z5LXRwgg`1KAfa*irOnlN`N+}PSeahWNpMH# zEkxJ;d(a<#rx3vg97J5ZWNArdiIsWV&-)W>2LT?HPe->0&o^vFLa%OWuTVX9U$?5V zfejQ?X|e?mz-n;a^uZt!@!@!QsCW=UAs?r zRTQ8XNK)|mhN);1*Wsgp=~a(a(w92^6ZpiaKY(SMu4&}wp%6OfyRLceC%f=xCKu3qzu@%oq+s|rI$JfnjjEiSl-yJ5 z&C_g*h8aF>XB<2ZUUb{fwE}K_wFQI*pmFoiWa1jwhB&aZpsjDf4n@s1PUvh=bKk*C zWaM%?xyG~!JU)K8UUYy2;p+0qDDAGskPGj)v*r6B2BAdWoLy{KH(Q7IIJhB130S>3 z=toe;P-9s7>Z@J+)~YG92JKow7C3C^J#6P|jnPB1!Rwqme_ipn11EyPmc@XS1EHFS zS%uv?Mosl{H8JrKN{f#G3;|qewLxT%X4^u_i>Fz}0Hd|^pCXn#=wA=R&w#{rDMJtI z*&o^M#SswkL;ycEj3FkB7P<59R9AXVo&TlI*!q9-F5_N$gO7st4#Kn4&qAwL1 ziF<%!Jg8Ee%Rr3Xvo9C&K|l*sRM(}efz`Gqe8mXaZaT$^<)VsFETikCE&uTWs3DGx zWx*Lp8pM_RVHS=@z8CgPNe)#U0t7Cd*wLtMBn#x}*}i7VPbu=sc9D}X;CdTPQJEKU z!`+jf%KLMi%F^;EZHM}qMQrSTOF?GVb_N7Y78K-1DWMeAJ>V^4{!G4ONMXe2mDhTE ztfTP05-4YxaNL=mTV9CBs$FRCk1*7;x1MMBZA(u3mM@oLRj89xoBa&8j~L+0i4)9o zcMIDE8-zVDve({jxwMBH6bZ;3Ry)bqL&Tz= zr-@}D>{Bm)oHD}UXpeSii4H8ck>-&k!B3XxBH|wa`0R6goeadkwK+w{@eWW`ozPTz zzJLC7khb;B?P!NKLSN9B>Rz>=rGQr;-4d34g-lkICG_Jdz1TZ|lQkU1`Q4g#k%5~G;DFt|mKYil=Ox%gkz zp}sQ~xzrDPfb_3y6wCkp-2UH`CHcu&cMky{iBt&{()hB;6kkw zP%0{lE%Zg3{OX9*0C#^X-QU03FtG7P>$saD*EhL3LBoIG*uYr6$~h!fMm~$ZSj8Df zMjOUCvdwJHWA0<`<4N}S{o_)406L?D-NU0J>!bFb$tm*w<_CjK?KyDg1?m**Q1F&x zvdA3LQMzE_Hu_PG9p8Bxi2HCoy0^C*C^v7$ywtlfB6`wGhENk7ye?;xxH_gr^j<|* z9Htl0oGx*#-6I<{2#ZdSh8oCICE5lv#lUjuc_gd1ND7QVuH)ol%3&KZh9aJHxnt5+ zoOs>TE@dPppAjuL+*mCi=6SCcMol=Vepu^7@EqmY(b?wl756n%fsW~wNrZd$k6$R1 z2~40ZH<(;xt+$7LuJcM=&e{1MgRYl5WJ0A1$C3PoVHme!Sjy&9C`}e&1;wB;C;A*2 z=zn0IKV9TBRf@}HLUf7wUPD*51(Z2OF-?aS8g9aGK19RG^p(MvSr*j-yJ~g`;DWQ@ zm>)jnf&y$qO43(PM>s>AzO@c0JT>h>Ml46?)9EG?S`3$r#{^%HIWQBrhVoRrP_hin zVZq6|`SdmdBU2ZIF_f< zwOk+eoCuOx{1Oa;*J8>1Dl~7xLUBf6U_0=tUBS`8K9P_XEDZ__5)FBJmf^FGg^9|3 z7|XM(3>NJ_OR62QE9Rz;RVXlwP1m!3l_XJ$;1bqgLzKSb;sdl;R{JK<+HjH+>=;|FgE)pRVZyy&y+fp6Kz6EOsS$nAil z)E&T0mU+z)s-ApBI_Q_!C)H$*TISc^zyE3l^#U6l=}c0y5DD6)m*t(~#`F$L5~=+; zg*v_EHOw_QcuQ?Ts3llUFA)Px%c8WdIf`U zwUs%DhS#-f$|o>`$MVsSLO%b>+YKvP9P6G4uKjRIlL29b%ULV zI;vtJ@0n`UcH@wNJC$W&9aQSf7Mw1(!(D8Iv#XggE8yhCXAO#R_FNiAtyG)W>@23? zS06PE--S7ya|$~!9cJKcg=H4nFtFurLci5Aq&A|RW5KWK6$LedAgKz--ouWjF;h2O zO?Mw&UeLh9uYdH;S-*W;4oh!-Xad3?2+(<}!<#uXCG#EYqswtbU1VA`t(Fd1C)rjJ z5lGFlCf@C`F|oel&7v6G+dNI|(d_Y;7 zIi!q0l$vFh7UBgcB(r~4Eszx?0!TAx7?N0Vs%j4vI4-k-CuPr6S5xoEY}gFyK$QZ5 zFl+%sE}f}p&ozcc*XpuDluDOFwyv<32n0)?8=9J*L&)N#`-cfEIBsP?OvmE!P#`P3 z@hBfK8ir4)L5}LY<`;lPOrAuQm8m+%)bj*e7&2v8JU`RM<$;kv7VYw|1KjF`CZyVq zQ;BY@l&6}Z3ILSqf+o^-g&8zYn3_A3W{LkCvcjxn$+1Y77M2+{SEkY<%ki!^B6Y-O z#IVs$I}{ez4=MCS2PZhR(SBp3gCLMa(6h|k^ocL8Ru{kfV3fX}Z|ww-Ig2O^a6ed+ zEigF}zE_#K%Od!Z7f<;&t0^|7nzl_Sh=Z84@<+;o2z#58Vz7S@*s{ZR6!Vaj%ya)v ziD~E^ClRVkP@NrNNF_?nJ4-HFQp97PVu(${w&6`I3 zAW}a~985bsE5sI6;-TNDBABp0QvlV1Lh;9`O=G7FXFF4lUdXVr@Yr;16ZKR+z$6;s zQ{9fUi9P|=&}ABh>jOeYeaE$}q>!#8Y%q?NM`0>>$kHHns3;l3sL2Rb z(3U|}J8`38Zwn!GrD>W0$t&Zp&F@&`D0KBYcDDgo*>h1|Ey3XydVqC~=G>q?L=edX zYFS8;47MB01Zsn`BMbKA>XvnjT71yfSLXwMPF7ayG|4ys(iA@%HNTFlpC{x6-}p6N zdhg{jk}pM3y?5#SItjDi5fCpE$>L`Qz#d^$pbC)=a%-NPHba*}>H#$&qo+jtvaTP)7PZStk*}35F|8HEoRnQRx;jguRohf(tGkLHrk{!MSDsI)YnZ^Pmmznq*))B<4J{?O=ge?P*=qdBr{SKk#JNQ z1vgFWb%qfIs)OzT;P!f_Pm$ru;d8nl8!A*+rGd(*$~T-9ll}1tW3xAU@}#MAuJC*L z0C;@^N&3czV9X-jWPjeFb+fOJoUQv$L{yq=a*L}Kd#At~5Bl0l{n zeH7>=^jr!`6Nz1t9E+x7hBY&EexVHXhIK%)k^qwsA*-id;Eark(C~&aV{~M|8FCKT zs0-mMgoGl>k#)iwf)-{t+Rg}68E}9kyIc=JP9+ezx{<7D4+gJ4$?_qsidkan7Hng9 zCqfv+1O!7he>OP?3up_hldSIDw+YYT+o!27ZtoW)_?spE>F+a%KZwEIS6_DqxSRs7 zGXTm=$d=h}<8TDfk%G@F4U>8n`pAr=6;CR%Ba>`9?1y|H4-O%sJ2%!5vA(7=JO&kk zX?ly;ss17g(X=9#nUWglspHq?j@f+YBG)GsQWG8CjK|mXGVC=3R zYy&BsP#C~;wC;oA{He+UWRN8A6vEWVGmaC&AtL|^>nR=S*@8mg_m-SSYh4o7h|5Rh z+5N2&1DIo0wnNW{IFH4fo70@u5TUL~e89t6qm;8njBvLCT0ODrN-b1qqwkByTP2d= z3u#x0Pu-GERkw}IAr@lU{IL_~viIH95L;=?Y4=(fUQbepY_C_Lo6EzVpM~N7wC48E zLHp>NA>#Mo3d}Fzy_x@bDfx6Ljk*Ot#qKu}-ktw3ZdgLkpxC?5r(fpz4J?9V`54+m zb5i>fCc7NelR{wncg9?ka!+E9YRr79{cE;0@@0$YTQU) zVH8x+&_YB1`T%(VJMj*;J3XT{mpNZc^^#0C*}^mP>=g<6Pl1l(q_P$Q2H6-Vr~qOV4Pn%(I>R>u8CrAVRH-FgLgmrn^!-+%wmWS zBI%O;v{5DdT?>bb1PlWdck;m& zG?8;NCa#=2oqHYKT0<~i3BRC?0{+JzM~g-D_D`yp+4N*OC-bxK``0V=Zxki%+)mDkS^pQ12u&|6wk0VNGM#$u+&mlTun2ByQ0crVttGAJx(LP92Vq6y3XSE|2J*}wga zKXbePGRmVA1~wR|#9mGR4wIkl+84^>OFy8}$=ce2qG0gZ=Sh{}4_e&=D03~pL5m{i zP(Ngin(dtf&?oVg55RB}PA>B3f9tXpk^5+?KN4NTze;pe{}w#|qx1ix&HhK^6l;Kc zYb~{Z_f$I6)+UnOFZ%7=*qzDvFsj)$nSTQGY00&)bYD$Vh z=Mp?E7@#elofl?nL+Ajyl*%veOj_a9#V>ZA19kX5)*frI<}B(>&E4Jdntt{df;j|DzDUxwq?|n{Hu!vR*H~>cCI&l7T$GeNk=Ng+1XBe( zfcX6q^Uq*Nu~&LYR2AFsz-f~tS7PbJ=!JATCIVojOo>QggJro0v5jy;xq3;fEzKkt zdb@do>>*3K#aFR`O2#+~Bsi;}M#`YH(+DnO1N5Hl-3d!{3G-A2gk&+M^dSK@3-NrK zytKdh{OIE4Dk@06#=(*W*_5ec^p=7JT_Um3)#?%xTs5fqy@kK*{is^ha)BbL66UmZ zXe+q8B`4Gc}VfQj zqdGkRB6Xjx*!hG7Eoh$%B)ih-SpfU!A)At?X5w7?>Lgj=RC!XmqJ@$`xkm$)&O{NE z7zj9>Wu5a1glJ6+sZqL&ku&qfJe_696xY%M+5{Q*03~s{gF+;MyxclXfz58vZb4r2 zGE@P$l^sMWnne@vmeP766QV|XTKw{f$_};3!{7iBk&;E3vrf2^l)d6O@R~&{!#Z9G zX{wlTM57#oM>Z;L3WuNo-J0C_&@>>~b{P#~_y_`gxG)DMEYUUqq0O(}&>ch-wC({e z9XT=mDtjJVyzNAu43=1Ow}&uu{|Uy8%0MEM-#-nIRG}=!CehVQKuYhrbe~6OK5OF$ zRDCn)f|R{sP1QnPJoZW14w{7rk!oBpOY@y=ix1R7IJkZobR>D$bv$aig~U4 zE<`A;fm7SCA4*XkiKemy+mlvxm*S7%=(0V0j2Cye5XTtz2x5PWHMEV}+>G zy7}=iU+iJQC?(sRT=??`!Z&fkLdo@J<0$1eA(GZuCJV;fWJV>y zia99Dv05Qs{8G83g^{w@@*~vZ2E5C3d$0$76^_=h0?Ay_FCq2?)2z|apx^r6Fq?X^ z&vU>OQWEXj+C6t)M+Gx;fk0RHH!H$ztpj}$<&!a8p{dft1imSbT$@s#(h=LWb3)Qz zYA8iL$QMWV@sfc=0CZ}{u_q6po+wOjpWrpy?q!;VBRBC7X7cF^bZ-eeB^f^> zQB`Z?1o{tEQvXOXqRY*(yLcw_fLf}o6r~WSG{{vGOiUVgD%J# z$j&gdK=e~U|J1hOZS(>U8Kj4rAvGrF1IWBx{2^Mp9Wk$g$C!xeTz`5gS{vz0 z-chgg;3v&I5-}eaJyclm^@TSC4tN8eor7K-uEcUJfuimwaZ64BEb%Suheq-h@Da~g zErZ@oft7xIYR7=)2~so^;HmQf-=SxIl&g3yZzQ)dn&;*|#&kWgLlX0cWP!F35QY=v zSB2>$;h|~6)Z{ZLT?-`a_JrYVoHNvsxvZ$p1q$y_cNN-mV}o;rcFMJONM=PnsDZIr zVC2MVapQDikYN5vCH)BZut{M2Q$T3})eTDtH9fqT2|SXZy|lnI`d{w$f~eB_D8UsS zn7lih>~118IeOB}ai<+1Y}Oohfff{nLFk}6M*X;93@U5h)p}SnK3uuK2q=fvx`Xyn zN>T9xkcy8E4;oi|>Ch|032-OHs zbh>nVJ8-&$cS0SUbBU)ew^T3qUYLo&ytrP?yM~iUh6a~yUEJE{s&}4%{tkwJ%I3pE z@~ClA0k^%03=gV<=L}RkZE7(7;dIzR{69fMY zU^Jt{-4CVPngMr)yA@ywB%OxN(9zlZeJ(P$YIo})tKSEG2nnWbN889d)`f#J(fV;cEu7)J%aN%~_$)Z>(fMP3Vw? zZ1PJCp0N}}5gDw$4Kt=g~m$O6&y+Kq$rbyR;oM+-R`+eqIfUr?P z^Tnv<)ZPK(iuebbZzaRTC4*x2up0rczT;GrI&O00wgD>Oq)Jp(5T~R}D0eh(ImW^V zq^(nk#P--V8q_ccE2YtLD|<`Rffk5wZr3k^DEXG3Po?}a=HOQVEB(M)*a!!fve8!z!Jf@HMHG$ z$9EKahtctY!Uf43{Inms%oP%|N{r%Wl8AXQreHG|%SgOX+R3KZ z^lNIxqQqP9lFtAjcNl}c`z!qTg|S|01BvwIC@gati68424l$8oM_w_9+~Bq9_mT)V#S**~fdp z@BLo^`s#=L`T%mcD=)EJ{Nzv_bWJw?j5-ReXPRv&KIY%_A8P(@L|Gh(XQ;v=Tp18@ z7r>|2AMn|^W-$2JU--UNcT(oY2iZbK8`9XdNGl$Xm&V*)@uAMX8u*)wDN`!HVV7d?xvknpLesf+@g5{Jqk@X&e0;gw;%` zRVef*D2U!@3ZuId8&n;3n2I&kYrq1EhU6q}s*ux(T+P&EymJ&Q7a<=G?M>9H*tV%h z23C!Wus=JN-k`lK#w861^^cSm_tZ{S?O=>Ak^9A(vodXxfpoNh_yg}l zM3JR4aSdggXNv$ftxyAIk0-;5u%ivhS2Q3>Fs1OA;)wuh>KVpmy;!!JQz+Fa)GQ^- zK!uQq2@hsSSp;nlsLM!C5tlR5`MNS6;IIr1_*gST6*BcvnIG;YyYGmmuR#K*= zW{uWUoEW*&=I0`Hp&gN!RL%z+39N<~#$AUFb$6G54ADoC(v^yC)==1-043o{yYRJP zyu`f4gc@N2j9u_+SNa&F=X+x+p#=hz8Lc@+1ki6W8YaIRTIemmIfy7dp&X{fj~8A5 z%MqUqz^ucP8mK;Nv?k6THibm?hKYU&l+RPs?&Z z1TK|`k~q+aFp8HT)feqXLhxS*m?YjEC#KtJaU7mYr$g!uMq%M1bm;dJ2e&Y7Q#L)5 zG4CQ59$X@{@~7_bQn`oLt_|6Bi~^4)#TQ}_xI$wrYB{JZq{uj9P__r4Tob6IC=Q}q zyu>Ec6-bEPsLB?pwBd4QBos#AOpVQ<=Ih6#w51-ET{XQ)KLY4HA`top_#AApi$CTs zpW(1RE-Yv4G@SK6yMC-3ZJll<7j}Q5jL!+2({qTggu>xjpO@Bs(qP7jm2sgow0Evu zUa5Pf zB$L4|q6bjR%lVO1em~M5oluvKL9?Kad-PZ0P0t16@Z#D(z;1?qUXOli*7Lg<#rW2V z0;mE!U_v+b8}Jit=ZwzDfy_G)d`c6&f+YBWELL)f^||ti_jW~^0=}#u{aqD1418FZ z=l{IshzcY0XC z`P8}4`8~_|wqkLI0@D1q?S++|j}8nchE+58NX4mY!|AqaMInDR7D9rWh0^j@qH!}( z0~#|rFu<)PAi@bY7dSWO(4;O(sW90AHT*0AgX0ClwN;lZ!_XRloGo^d(oR=yX`7eR z1>XR(6OY&6+M=Sd75vQ1EowgN+9r$4?EOtY4*lv1`$Lmj#GZ-`YDS!BGyYhnrmf$W z75wW^{L&R&KDp~P_kfF`!J&oab3foYFq|9uvJhbD!7kN%bw7DktjkmEy!5W?OT(c% zaGJp4Lp{#`F8Kj@Z>Ss0O%0@L z=_o3AS=j7D=%871sN3^>4%ZY_={S7NJKB5BZ|4RR zQ$Q7UxvnAL0uU9+9>1QsfJ}Vsk*j!!RFk+XflYjCk7$vTJ_2SjeXY~bvXqblWkH)8 zm_H8Xf6>cR-*W{BN_PLc7{{{Hc%%?Kj)Xka%N}5vxmf{!6{I)`F4FaaRen>B>7{M7 zFH;#D`{Vs0{<=mIehp`2#J!lZkG~;8{n4Mp0vT&&EO`ri*GTBE<@9%eA2EM~pMK|a z52w|kkFT#ceY#i1{l$%ZzzP>fzWZ#yiM*F4I6Ykr^6QAfqcIma+F$($yxTbswfDlgY zjgc~blW_GD#X`_8!LVXh#jx=VfgxneOSO`fgCvdo<$IRqBZc=+iQ4*V>q}zr*5$0y zCjk@J6MX~(C&%#*)pueRdgDq9e0j9PB zH6wwc{sz}!wSk_j`47%~w)U<~RoFV(39zI~L8E>5;}$1S)B!fUVwJTcH%^mMu~pJ2 zZPlV%ldph=kh!imgV=`k@d!MVYlsVmU#lPh>!3kmtG!ivoX)l=Bdj|w_Wt{f2|>{3 zNSJBa$L3sEA!C~DNco&iVHGD>@4!!uXNlu3Pk`?puU-1z@$Ouu+{YYp2%M>$YNN-R zX21B@IoT(UP0b=3v1js}LcOnCb?I|)r)^)mhCCFjNA8R6vyr}%?s@mhmn#KcH}bC% zW;QKLy@waI1`|<0|FQ+D!u#`z6h~9hlBk|$5N2e3gRK(2L6k3test;wIlH<@Hv+Qn92fx zxYGjYk#gV)nx5wDl36YZW|c(eQM1iTFxD$M4EWQ#@Ikmnos zgpO#tUHZE`YJGE~gbEs=MG9M`5m7I=qR>=1V z|2UtTmrRK@T1SpqX-PKPSeeIE#~-b^&hu!oPqmU-_+LgJG;WHj{q2!SZb7%m-xQ6! zprUP&%cs7y)ikUvpz?yHZLTdbd1_X+sV&8NcR6UqFVOS~I=djZX#X^7>faKhzJ#Bp zdXF`4{uJpL|DxC2*VjB(7e2@F)x1`h1r&p}vA@Wx#D!ct;SkNl>2{9Z_i?V?2dr?D zEd@K)v~=zX&B$_7XuJ*Q=;ZT)|s#?fm3jniC9CpukXut5IW=yN2N`|3UW`k#rI*J(Xog2^D)Y~x%W47}h`A5$ zmsV?ZyTV#5oJSmcHHL$rGkvPMqbhJO9T!=1UlzT!b*#&pQAD1fXRNT)LXTW-KH9P5 zqX6mHvf(zeb3x zEXeM>NHfb5+$HJGc+3)(nv@x8IBm+l(_C|(TuZNmP2*`>m!y$tW2AOSXO2r{YZStF z+Ccj=qg;lR(Uy42#$^$lL6qX^YC5E}J|Aurs@Ss9U?as1KZVF7dFk@jU~#Dse2ANf zF`pf3Q(VNOxBJMQUQBKAVH^sz485r#JAS)NU4%V+&Wow4Y{!*St3Gm=3c?7!luRLJ zg8-;Jw$eoq@LDU6z|5f3BMW1QW;(GV0rdsOsTMc{h*73QQFwmZi;R`xCLKjs4V{8z zpkLk}#kb!1H{sV&A#105ow)@<>CPfRO1^->7RCgfoa0qjRbtq>1#mQA6~Zmps*9$C zR{@xZBNKF?Mq2ai!d{@VHsOXn&+e@mbit@0s%m5tD@)I6_xzwH=z`O|vOpFckg9%m ze}V)thirtajxb6>mow9(IM=w0UNx?l27;MU_eGA7OLmk!q@j@SDNnEli|fF2ROYDX z(@@F^{@`$zOC}1MbT$&$^l@;LAtU!dl=fKGg;g3`;8!l{0*2`6io3n)3Z1lwW)qSMX&&H6B6op0BOsY^48CdE9CD;j|AytFc#uUQ^dVqKV zwPRM8q8!llV^uFELm7t;3^3M_RLO)8_Y+j<6@LtI9XsF1+}4a!SAPqcNLFg9^)`Fj zSgEmL4kjDU(UC-~)XR&&6b*YRSK8_SzPffPc3;=6(lfX%ve2OsF|@(LglrJAy6j&3 zQ53Gan!U=F)Di8RkReOBn>zer+=(TSwGnTf z*Rnzm*U6Wo*mtLhu4%hSke^_>nlU7&JcYPyEYiWY@cQ^DiF~Q?auFs3K@+K8;kuMg zwuV5kYV-V`8Pa0Rn8E0n?XNhH*Pzdpue#m!P-{kDo9Kc7o!U8?)FJFJY5DV=Q*K*H15|zoaeZ z;gxIT%0tMEjrEbAVn)F1EeL*5dWRT{nl;)MIguR%znlTsrb@ryC{?py2EGI|CFryT z!uC0_J2yACqMsk976rAxFnx|V^q+Qn7Iu;++gH158K^3#bC1z_krqGEZP2cH2SaAd zbWdZR#Bmx_1o4@I!Q%W3n9Tep>w1BA*_y zE*4?as4ov0?r$f9#I~7;2el*Mt(EV+zC5+-Le^6`%OR@XZ!})>Bn}{U%S&l75_70R zb>YYVd*B6-9;SVen?o4vme^s{;3Lh@2$FpuId@#!0V5XGt_n?Q?>0Aj{qI_?>+^xw zpWFpX8(TKSTB&wjom%A@uC4MfE>)(Z4|)#^vatul3d|Q&;^cbIOB)Ncc@bD-%Z)*b zPq1FtofUV>ei{WDtc7W$-qg(JrT|N}TkwuR+3~h=h~$sN2i|q+rc#10nyXjPFTte^ zX{QLKnDAZ)>$oJT&c$sbSl&ZaSmvY;Hy(U_{137EqvMIR4Tz3wJ*XZVoe?g>F+901 zYd1hLOzdEDvb{a#imlA+k7IPm1n=9%CPPZiV~iRw30G35qwSMmnzx? zIb+c;+iZk_2SHQzZBl&ygxB(x$tptwTl(*r^Cng#Z?J6bC#<$TK!Gh8s*s1u;;pQX zvRHWJVDysYrJS95YnW<`E0@-JJe=tSHzbs13RN2hQt&+7Ng;#3e^8-n6v{%EEkz8t7b~IQ zE0;F@wojhK9vK%HemcA8cBMI&s4v@}lHkJhXfrM1xj8Ej3nMj}xoUbosn^ObCdY7b ztp_(h)oP%ekys;b$wHPtmL%paSC_hQ*ReRSJSSzB+0-?Cy` z5(TS>p0S~tJG>R~%V(`qVL47z>BzEAo2^%wsckeF*O7_tEk%rL^AH+1}ZpX?fat+c#`9u{zqNInLk*PD-r4NK?HTgbbEW`hdk!^+)OerVxh}0<5*_sCkD)>jE>PECJ(`rs&vQSqiBi5#XrQ+l@&S1Yd zW~|6Kcs&JHx%qg0uNT5t*sdKbwI=mIMyH0=l~^7n4%Gx9Hr0&5HEkKzFe~Ccz#3>T z8x~`%;_^u&p%ch^L3|%V4fmqvp&jfpm{lcT_z+Z6sX{br`z*-z**l( zV*al|m~_3NXsFj%c&dvLtk<>Lzb&cp_>bRZ93&_w^(yYX=jDDbQn73PDp7cdU?aL*BL*VK;Q1cou@ z<%G;A5a@!4(@Hfo`NlXWafmoES8>Q#r+J<2e z(k-d+ZwTe`VlkbBAvPyD3t3`rz9J*x2ndxGh-PCkPFw{eMk~JwiK1`nq$^QlOp$CYm2hBso=rlg&n>nQl`gxTL!*$p%b2}P zBf8is+YZF7+2?v68)+4;J*=8pE|v(|x5qBE#a{YZEy5HT&i4U?GLdWzRHt;hud(O2N=D&%P3w#yDOqn~`& zeDzN3*cbj*P`#yuR3A_4HXNW$%i^6B_B8n4*HeP8ZuEu>)A(~TY$dutg3yjiq9{YiZ?V#Nt_LA)uWe9>rq zOHY``mM3W=EdOW_B57D+$7}l9V%T!+IC(oHe|atxeT|j1b1hi?4K?{V!Z>rS-^1@8 z=l5&k_Pl=J`@e>J5(Dl*2Vs8TAB=x%j{YCy*#9<1|Fiy=1;>BzKPK_(|NPN0lh*jjF#w9UmGnIgJ0%yOuB27j%sZCTS;t8-sn)vVC0#XPY$6p_koe4npSvG-=%AfGn*3X6--%4AUZ@@3_ahu(H#@uo&n zxre;2?qg+#zsr$OUQ@T-en-C`fQbw@O5YhpsEn&jzpAVR6zusmS^ltOlApN`RY_X~ zI;3&Oo?-f&#_gWM0U)t5HI+V1(@V7aD=M8lFE-^3tyu1#!4b=jvwO=Qleo`7FcV~*8oYO?n`U&ennfyJk^xQJE)AJRf`t%;S^ z`rFA&buF1xT+8q4X}bOSXMlwFm_N31W$SwnTG%Fk`{R(@-(`}(Hg{QC6mo|3uNnK`R*%TkSiL}N;=X8pxjI>x~k?l`hvnV_S^&7%)r-bq$H-gKFPQ1 zbPE7d;16MAoZJ~ZmW9r&iK%as6H9IJyyvmI?!@7Px0&B^L$k9cVQn6%oB2rdbW;lM zzlccZ`yY zb%o6E6xNkO*s7dVe9GAbbpt0G z#S(Rq!VJ14{_28x!6FY~v;`#sqGFDj(~AhsBH(PoQ(QJD5bF{JS}}>MFJl;{^0(8u z<~p337P0WT1+Z1U!t9=g6%jgQa-J~nW5YY*0L)x{M6)!a9E8i-C{Jf zC1qZ3Ju4q~Ov~+1ZN8NUe_VT+rbDnTLJ`I?T#rteXL)goXPMmWCA-9R870GE^e&K= zpw5b6wUSbaZMnvRYNF}#a#U4?33=bqiSdbQXve-VTu_dpjnWS-N2$V}PkQ+f)M1ce zS3vxWdnXr>Id@KfzEX=`WNer7%8^nn%(fsia8dL#VEHqwPSO0AywiDTzw+?k8iFB< zR)SiSjbbU1$53GloU_PXxbqpPwCAKk3%xQEsvusX%Z|>Y8 z$hFs9_1*nu9z7Q<)-#+=`|YAUlQPQTQDIKJ~`Bq9o{GoiVlM9 zks8$P!tjc6^$GbkdQ^iYJfTIohMEsb10N8G%WXpn@j)e)({uf8Z0=1zgBp*K#O1^u zX68l$9vUC+Hvsb1>qZ1096EvnKakT5X-ph$RjPebuUt|6!%uOq_mEeA5%}5C*LtvGPt2nN(CQ4$k*B4OxOsx=&{*8s}f87Kq>Ke&M;dh zo&PMi*My#^X$UgQM1Xz)M|lxbX0k8gq*DtnBErf`R9lR-7$cw59vzICBcG+YYO961 z@K&yAg4M?gGu!?(!lhm1W9BwIV6NaTS$&yXa!Jk%9cB?8mnUqLojR1UZX#C>ItR%; zG)_#*l;PTNF=kHof?cXZ*z}OqDTAckDzNk@I~rz$A&Yfttt9qf4rI|khDIwDkaCU0 z^{&56PF>BFbE~99Gu7d=+;EmYkd`~1b2M6~b&`{6A-5PHL|v%pwC}5f(ZX%K%v#z! zEg6NIPO&ZISs-$A9CmDoSN8Gr?>36*Qv;JNW5GxA`VKRyHULY~tkcJnk=aXVvn93a zv^?!_jh4r?GSp|#s|CM$XP*rVPo9;XwTDm!OcXxUzDIJ28bV)ZzH~feD?t22ytG@BiG0tF|Jr48RYwfkyUTe-hzpu0+vcJD^ zm1jDyZ`nlkG~eZbK*YsgFr2dmlDOKBhqZ?k=7km~+p9rBS&rhDAs$Hv&e(WQ!e00V zlb%AQAZBv$2TUq;OdBu26sDHtep#r@$42JkMaSdG(>!|=k-GdYZ$&d{JuBTtHSPns zcE^hIssoLqm!8pOT>gS;G0lDr0!OWbLxQurlvb}W9ogPdRow||T_}I_kmBf8)5d6O z(YyBp>hTvGD%o=7(~un0z*A_m(7@?eqIj9_Z7CWaJQiz9s3cyFpNShe9?ItFK`?E5 zpXL0a95Vq^BQ_oMGCLWT@+$t4Li(ln%P#6H^nKH?4A)P(S4}cJGs3C#d>NI@tW81s zij75YC|**UN#rEut6%X-TbDj=VoNPFvSB&m5^?dl#GcBbPZ=!m=GC6JODb|pSgZCw ztCg5B9PuE~OIR27yM(kMkQ(!Ayb3B97aDLpUe2mTmH^RYbkLF!W-<*pORgM&3RY5s zg->y6VNScDnxd0{AC*!28f+z{V4QhQq4&4FVZ3*R41Ar5Um(?ezKG+&&%9bfIA?M} zA9{i@<~yk3Dfs~1n4 z^@R26Nve`GN)Up+_acpcQyB{nAx4RYRdc8S$QIP7c?E7%!}0X$^5X zswW}mTFr6Z)wAfR#4*LC@Zr(ZX24543MFZLaO51*p(z*}G4P-52sT^khk#jOeWpzl2o!2Cc=buDucQ-a)H(-<0~A zgN{F!bDw%2A?63Ua6WjgUi-*deC;(kwk#Q$uy_N+Jq8TN*`sG#8s2XOELS-*0rZQF zre$(Nucb127C-ncK<7NfF#}p4#eG9J*|x=lDFdOoevYABGpHWRu>Le6p{46>jjd0G z7CwmzOJ-9=OmJlAfYKD!tWE4Q+Rn^}SYHVd>R6lyQ;$Dj-f}?qp3S~~{1VBz_iK1c z*2dOew4A+bma@?hLk1IUwYvdR&Bj&>_7yn$jeN%c>XPhYlwwjL&1|2^Df!~kgnolz zpp)zZcqrt1p}b#g8uGp$$8}a_Es*1sb4Y2m-fmwylOT!MukmT~H0658{#zf6@VAP@ z{HxGp_0wN$i4->&2cq)QAF(TC=XqA-%_F%|KF^+54?=Oy601KXeQEjTa->iF2*>${6U zNfJ7=tf9ndv)#TaYscj|kiq2aYO%3%V1#Pb#&v_gt})q~3Rhftzo*zb__9d)<;-T` z-WTuTJoD#xS~Ds1?$oh1JNulMim_Y7f#0$#naXiiT}_Xdp-MF|)K_C9wdvXyv%5-y zv=&BXwHKT?bgA13%ay~PkCV5H@RGHY+XLaK2QaYt!y;+hp#!6L8qp*MOeFNW{mIzH-2sTmXPW$mhoITa79;3sj0B`5yVnXsAFeC z9ZDFq4NNqb7#1P`fpMSN`T z*uXRg|6DEmNOyQtiG8>m#6Kv9V}lC`@K`{D=j&kMqDx=%RXm5Cs#?}NZ&Nckw0cO`W^Oc`hPtDT{_5b0WTY)dZ;8 zJ#&KTM2)%{3rt1enE@N&5v4?_1@OdUZn?U*`66nqHR|Gb>0h!<3W-O90hbQ&k# zOFNEtSV!X$Z0I^S&g*i3_`pPWc{K&*>4!C%EUetBw<7yuo5gc9T$B!axCqb{QTy(W z^#1NanWKZ7@1Me^J7Tqd!?spXS5Q#58l7Q`+!XVcPq|l#-8ws1?x?w0nkYHrBUNot z&gf=wtU(uMWI=R+;ukx_=|b$b&(09eFfUVAu=K8v`NO*k8p&oa2Sswj#TxpIf{Fr@ z(tViq2@(`F5I&mkMM>FQ7+j=3>gNofYMj8*I`Z#9&fih;50<=kIcAgLo|~R{pf)v` z$|oWmF>-GO%Lm=Vp`&b&hkP(X-7I+NEov>r*oQCfLrW#06P5=1aM%8QwzJWxUUgbM zd}6z`kDyFi6nnV*%hcf4OOdN_E2=Vk9sBCvKZB25VJPb7f`2PeB0RwFjZHLbsud>B z1dyZbAs+;_;)8!^A2&*6PLx0dJi9(t8H{=T&na_6*MA1*2zFChxe$C}qtkh{STX`B zAK>Atx8R3aPNf|W1L>EQBb0Yx*1inT$`Ow9$`*F&^q*O*EBGvZHcP`M3CH>lva- z)+;y$Y&K1gBDaAnEYFcRf`f>`N>F46K07E3qQx;O8zzS-d$r5*U%HQG9ydU0Gy|IZ zXJ_|zwLg4$B`^zKYg%l)LC*h63~KaHpa(1l2QE)&L-BX#saHBovuf~dm$X;TWgZ3^z|^;enzj_vgsX28+P== z1g#k33Mdl;W)o_+5MbR=1kQpO4B;wz`dnuYH;y6291Uu!S|jLym8>25G^ns+C`|i zU8?IW9*CTp+=#b1v3;Y^#gnj$#!+9~-|sxPtwrGTnms&B|#kyO6t`q~ZN) z-8vvD?Ni@K@@%2GwR4uD&%*w#xr>S@m~0^g3?_xG3yIyrQ6CRV_fuPnl-F=d`^?AX zqN8(~H)ERx><1xs6#_(7nFZ`Zn_$C<#Z#QKAMgjK6vXqkHN7lIM;2$a1`)G#dsp%3MXqQ{wZ zwi49qr;`zM68#yL*fzn`Zy;0UBVsAP5wjv8#}+Jr6m95Y0IfCV>V@ zbvtmr^LW8tUX$RWhiO>rp3Pf?u+B`GXp!>LMLVc9;05>a2 zJg&o$#;ZRz!6o zM+aOFeHgyi|3y;1HT~s)0vwjT4$uB`XqNHkGX|JE3rwSFZ*FXNO{*$x@XYAHF9euB zOPxR!tj6$=>Vc>ncnWFF6=Cu99TnveWvY;dB}fO*=jz$8^2oqZvCVhm(a3G)qhAId ziV&ZT=VdcI9fO~7JK{PfaAVnG(*ZCt_Gm>VlrhcJCtGjNTzP;?wh=9v`JIn#X!msA zrLV3}(zQ`NaiNV3U3C~@kypU2h{+$9cwifsq_f9O3rdU|0O>qFI?u;RqBqZNk7CJ7 z&bN5b6@lA2*K)iFnm1ZEIXsuEH-G)9!0fG@{es$9F}EXXf&2jKmJ2XsA)#caL_WWR z%TUPo6YkgK%^KbYtN3KnXElrVV?)7Iiq_SM^EO=WBOg{NQMP1~G<(Q$3etTtTooqz z269cn+^c>ZMaZxzD5hOH3l;p01qzD($UBz$R-@*KY#gO_`+f$w%N(Y`qyzct>8$qn z(+{*ZcOuU)#rtx|LZeXJ6=uvQ*lAgZmS|T@5O(s(D-a@Q?ayr@5L|2|Tg~@b_c>L2 z__306iq%m+V~qF|ACYkfKw@2R_x8;s&L%G&lTqswsbbZVW)adc+qf&Yk}xvc$5*Hs zagVTD?4VmRkx@0Huq5{>Ow41}GC-pn#uq1j{9>W!C#!^^&O#Qorn9Wg!-y6qM@Hue zltD~1T;WZB6p^cj=UtOntm|I}@3!o)2xEg7*X)Edk0Ky-fK zlJUBV+WA!)1|scHcmS1IS2+dMSbQ}7NBA4QZRYmjr15bEDB4JAnZ6yNQiy?}GU=8m z_LO*ACAVB!>ot4aZyUb(31GXc726pp{V9T{ZRe%vRC6#z(=tk)TL`C@5^K44rw?Rc z8~V=G3jbs~jxAArcF7d=(p)!m3ZHE@(5)^HA(K&E$5purbnHLtrd+b1-SlP`yS-_; zs(gPp);eC|BcB<--$ZA`Au9>%nZ%-H1n=5LuR*yuxjlpLK*OW~vo;pieYmOMNo8z< z+{>&h_|o*b5d+!4{Bv@D%CMklf!yP%?_o%UGk~!?^Q!^RMVLaTwYAdnjP;IzQ{C?c zuv>6|@i^+h&RwZ;u|OiYaI_~Y6sX_jGX0em)A^-l%B=R6_r`ejX4>>UJlGQyzhV~7 z7UEBjwMkz-AT;7Xgt~{a*NJoNIm<$|I*%{rk>Q^tFv!s@@a#Mxb9>7Mb?>Az3}5i# z!9W1HO)g>Q5n&fA5aAvP*WA(9Y(Kf6g1{H5*0SPOUN7o z%p2P2;4o09l~86ea|C^7znvop!ESRRyq*>}tr7vf(QOR$_V6riVv1WZZMV_ zKij&hvKF1vkP+LX!sPq`E!kNfBc7y$#~taz9UtA^7UgprsF_)y1;~Ry_)q*ZW1d$u zqTCy4I+?UI;f#B&DRznrAxfgrw=NkepspfGl1l)dh|){D2A1IphvFkWOeauvL9~n2 z{o`fCZZJ)G^evX4-41DP47S>$`O!em#-`S{Y8;T=5#(93h%qaig2 zNmzuYSAr{EEKnEE-X33eLrh`|7yCHEB8*K7K*Cun0!UEEj<%37yhOGHNSO6mpYAIp5NPaVSc9C{I!#62fF6mIEQ4?8sMEpE(o=9mky-V=L8TK-b^EV2!m+2m4c zE`)fOy&l!gie&EN`Ek<@>`rXD)UmsnW@E`k7%Gp$r;^e0*w*1J)T{t5)P{BLE`2p` z&RBkKZr)Qg@}QG7xp=00&A9}j zX{i}A7m@cV8btO(?xp&b;}E^r2}nJz3h8y8pJx=@4l>nsYb5BcKF*{ToSh4=-9g0Z zb)Ji2yc{J+v)`fAIQ*0+$Ty4SWD6T^=&0j{mFn`11?MH)Q@yG|joP^5P4BJ0GU{b9 zgG5``R2p!< zw1h!cv@m@@tjbOb-RiMdHA%4np26r3-GoG1E02X?W2~^SdUx)7d>7iq+4=HpfWm5R zCpo!$I^k@p-O+Tb`|;KJE}tjIvCr&A$&(u1aB=^IeS{I#$b(3GPC!WZft!euv0VQL zC%s;qM6RkX^&1BcQrKyq7b0%POVNLs7aEl%;X^dLxIf53jKVU zglZ0=okrM<2-%2jaNEZWGoD1kMSq!kv-+|pFQiQQo2AI5-1Si|v-Q{q+>$bF{R5vZ z0C>c{yy0gt>F|T%0-#sV5Bu=zmfMSY#~DmRI;%W*QyMF`fy?`8FxHofRh8L(pd9#& zb#iol1;`+wfFl3JT0dU7-!|pTa}F#4QlkMg*>x?oPL}e6FZUHIvy|EIqrsYGWzr5$ zp@6iWZVrWKSuy$KeXz2Iuw(8;M-&mgRI~;xo%M(6LqJY4BfqL*fgm;sdhZ8$%%bha zV1l61PHI34+lfw>Ys^~&4_$@Gbyk96Fef~;C{I}nK^DJG4XR|F)VJX&^V9dQZ-0oF zs6F8V+NWkvnni`AZ{LI}_J-hjhS~u)LLWEdY%H7*2{Dd=6*hs#TVU(J{fIq;An{!+ zn2E9-@ zZegpT_rXE8G#>nRy1^`PFscA@zvj@9dGerv1~1twD#bfWccCk}f9M(4R{{G+Xdpid z4xBBuZILxf;B5LMn~+%BC-~XsWfrFfI9JkG)0Ea%6w{014m)B|PL90ub8p2(2DX-m z8?3bf3dwMt1y(-_Q2g5?ZKI)b{kntGy^O zp23Ri;p0|TF733ZsFj*xQr3P(ET~^qr-%Ob<#$0~iCatY$H(a5T^5l6?ZBtp{7vXQ zswhdYscNN2y}nq5&+3AbZR>Vge}&Z;H@7ju4fN-=R2H-N%(&1+D#e>ru!x5(jVW>-HDcn3e*n zX1htG12i+^(gW&O{DdEi>_@-j^(U z5T3QjimlU@`B}qoK9=p6o#<6w?iB(~(kClUtuxD(6}y;MFESngI9m=Us@f$T%|J3o zaoL+0g0JBW&jdJMa~}E=kv)HGzSH0Lgd#`o(Qq3ifipq)M6qS)7`H8v+*#2#r>--C zY?X#Q0X!EvL9bjjNDeQq0*V^6J7^wA%Y*+*DXL{8cs1lFa466*l`Nh`wO$%hdBqOg^;OhX_VF} zQ6#S&_o-~%bm(%qpZ1v2$Y;I{dKilI)ZE)G*vKq9Pqb613ivS`X=&7f3>Zj- zKSd~}t{_w6Q!b&AvGTg_Wb@uJRrO;}Dx1|NiU&@Kn;TRk$|Y!rQcdH=8}F4%Uin(t z7W2uCLUq1ke+IBGzen))VEU<<)I-U z0r4L<3L+0=Bqfwp7!@S{(bc_0k~d^v5F7A^<(4Z9bO;D*TT>>}zxdIZo>-bQ-Oxf5 zu{C{R1?I8_3!WI;{AA&Kx8;|*Sxc|L%Yq3oukW?i;txy2_!Z7iCCTnOhujvVxsL8s zfLHR@l372@_uj9Z|0RHCOCe$cR#W&Fklmg2`(30gFlmnpxCv3<{R00jBpGmt)jxOF z-$7!m3g&ipU^Se7bt!nHfCVe;jepb31OcpxVKAgDnDqH}GqWiE0P=4v zM*~~qfA#gBV5Y@bA7+3DzB?F~`&QR(f^X2@Ud?}D{yE%DCHvdM^n&(};grErGS5tZ z)0sC#(phgcEQtOOkp8?$H#Mq-ZUMzJ{sGV*DzM)jo;M|3Z%-!PEWbznP2b&=Q@riG zlk>lv|J75!(1^Wz<~L>kt`!-7SU%tHo&RgV{pS2{s#)D0Wse1JLHtLi=ug!I?>6S9 zLejN_$q!o>{RPthtd(^a_okAL;4NH8iCeh;A2p`Cpf{CVu0?u&n3B{j(0^wQ{z$Ut zF3L@@iQ8Q&Df3g5{|HR{ZyGUoac@%YUrSm1Fhqr4PyPM@@$21lzgbIt%?SF#R&{=X@po9`C;Xsy0dCeKT$g13uui+5 z0{puM;jR|cUB@?HjlbPHOP;@U{EOm-yBIgK!q+d^|FClJUt#>_!rsi?U8j_P7-95J z-TpMeeD`E;CZujp^Iu|r>h)Jyz`M?GhLx{#T0cxN{^!pBAj5SRyKy50$qLSTURK|Fca-~JC(R-+UE literal 0 HcmV?d00001 diff --git a/backend-spring/gradle/wrapper/gradle-wrapper.properties b/backend-spring/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f78a6 --- /dev/null +++ b/backend-spring/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/backend-spring/gradlew b/backend-spring/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/backend-spring/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/backend-spring/gradlew.bat b/backend-spring/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/backend-spring/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend-spring/settings.gradle.kts b/backend-spring/settings.gradle.kts new file mode 100644 index 0000000..61d5455 --- /dev/null +++ b/backend-spring/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "allowance-planner" diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/AllowancePlannerApplication.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/AllowancePlannerApplication.java new file mode 100644 index 0000000..dbc9452 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/AllowancePlannerApplication.java @@ -0,0 +1,13 @@ +package be.seeseepuff.allowanceplanner; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AllowancePlannerApplication +{ + public static void main(String[] args) + { + SpringApplication.run(AllowancePlannerApplication.class, args); + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java new file mode 100644 index 0000000..72dcb63 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java @@ -0,0 +1,506 @@ +package be.seeseepuff.allowanceplanner.controller; + +import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.service.AllowanceService; +import be.seeseepuff.allowanceplanner.service.TaskService; +import be.seeseepuff.allowanceplanner.service.TransferService; +import be.seeseepuff.allowanceplanner.service.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api") +@CrossOrigin(origins = "*") +public class ApiController +{ + private final UserService userService; + private final AllowanceService allowanceService; + private final TaskService taskService; + private final TransferService transferService; + + public ApiController(UserService userService, + AllowanceService allowanceService, + TaskService taskService, + TransferService transferService) + { + this.userService = userService; + this.allowanceService = allowanceService; + this.taskService = taskService; + this.transferService = transferService; + } + + // ---- Users ---- + + @GetMapping("/users") + public List getUsers() + { + return userService.getUsers(); + } + + @GetMapping("/user/{userId}") + public ResponseEntity getUser(@PathVariable String userId) + { + int id; + try + { + id = Integer.parseInt(userId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + Optional user = userService.getUser(id); + if (user.isEmpty()) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + return ResponseEntity.ok(user.get()); + } + + // ---- History ---- + + @PostMapping("/user/{userId}/history") + public ResponseEntity postHistory(@PathVariable String userId, @RequestBody PostHistoryRequest request) + { + int id; + try + { + id = Integer.parseInt(userId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + if (request.description() == null || request.description().isEmpty()) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Description cannot be empty")); + } + + if (!userService.userExists(id)) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + + allowanceService.addHistory(id, request); + return ResponseEntity.ok(new MessageResponse("History updated successfully")); + } + + @GetMapping("/user/{userId}/history") + public ResponseEntity getHistory(@PathVariable String userId) + { + int id; + try + { + id = Integer.parseInt(userId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + List history = allowanceService.getHistory(id); + return ResponseEntity.ok(history); + } + + // ---- Allowances ---- + + @GetMapping("/user/{userId}/allowance") + public ResponseEntity getUserAllowance(@PathVariable String userId) + { + int id; + try + { + id = Integer.parseInt(userId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + if (!userService.userExists(id)) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + + return ResponseEntity.ok(allowanceService.getUserAllowances(id)); + } + + @PostMapping("/user/{userId}/allowance") + public ResponseEntity createUserAllowance(@PathVariable String userId, + @RequestBody CreateAllowanceRequest request) + { + int id; + try + { + id = Integer.parseInt(userId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + if (request.name() == null || request.name().isEmpty()) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Allowance name cannot be empty")); + } + + if (!userService.userExists(id)) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + + int allowanceId = allowanceService.createAllowance(id, request); + return ResponseEntity.status(HttpStatus.CREATED).body(new IdResponse(allowanceId)); + } + + @PutMapping("/user/{userId}/allowance") + public ResponseEntity bulkPutUserAllowance(@PathVariable String userId, + @RequestBody List requests) + { + int id; + try + { + id = Integer.parseInt(userId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + if (!userService.userExists(id)) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + + allowanceService.bulkUpdateAllowance(id, requests); + return ResponseEntity.ok(new MessageResponse("Allowance updated successfully")); + } + + @GetMapping("/user/{userId}/allowance/{allowanceId}") + public ResponseEntity getUserAllowanceById(@PathVariable String userId, @PathVariable String allowanceId) + { + int uid; + try + { + uid = Integer.parseInt(userId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + int aid; + try + { + aid = Integer.parseInt(allowanceId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); + } + + if (!userService.userExists(uid)) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + + Optional allowance = allowanceService.getUserAllowanceById(uid, aid); + if (allowance.isEmpty()) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); + } + return ResponseEntity.ok(allowance.get()); + } + + @DeleteMapping("/user/{userId}/allowance/{allowanceId}") + public ResponseEntity deleteUserAllowance(@PathVariable String userId, @PathVariable String allowanceId) + { + int uid; + try + { + uid = Integer.parseInt(userId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + int aid; + try + { + aid = Integer.parseInt(allowanceId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); + } + + if (aid == 0) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Allowance id zero cannot be deleted")); + } + + if (!userService.userExists(uid)) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + + boolean deleted = allowanceService.deleteAllowance(uid, aid); + if (!deleted) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("History not found")); + } + return ResponseEntity.ok(new MessageResponse("History deleted successfully")); + } + + @PutMapping("/user/{userId}/allowance/{allowanceId}") + public ResponseEntity putUserAllowance(@PathVariable String userId, @PathVariable String allowanceId, + @RequestBody UpdateAllowanceRequest request) + { + int uid; + try + { + uid = Integer.parseInt(userId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + int aid; + try + { + aid = Integer.parseInt(allowanceId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); + } + + if (!userService.userExists(uid)) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + + boolean updated = allowanceService.updateAllowance(uid, aid, request); + if (!updated) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); + } + return ResponseEntity.ok(new MessageResponse("Allowance updated successfully")); + } + + @PostMapping("/user/{userId}/allowance/{allowanceId}/complete") + public ResponseEntity completeAllowance(@PathVariable String userId, @PathVariable String allowanceId) + { + int uid; + try + { + uid = Integer.parseInt(userId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + int aid; + try + { + aid = Integer.parseInt(allowanceId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); + } + + if (!userService.userExists(uid)) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + + boolean completed = allowanceService.completeAllowance(uid, aid); + if (!completed) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); + } + return ResponseEntity.ok(new MessageResponse("Allowance completed successfully")); + } + + @PostMapping("/user/{userId}/allowance/{allowanceId}/add") + public ResponseEntity addToAllowance(@PathVariable String userId, @PathVariable String allowanceId, + @RequestBody AddAllowanceAmountRequest request) + { + int uid; + try + { + uid = Integer.parseInt(userId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + int aid; + try + { + aid = Integer.parseInt(allowanceId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); + } + + if (!userService.userExists(uid)) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + + boolean result = allowanceService.addAllowanceAmount(uid, aid, request); + if (!result) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); + } + return ResponseEntity.ok(new MessageResponse("Allowance completed successfully")); + } + + // ---- Tasks ---- + + @PostMapping("/tasks") + public ResponseEntity createTask(@RequestBody CreateTaskRequest request) + { + if (request.name() == null || request.name().isEmpty()) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Task name cannot be empty")); + } + + if (request.schedule() != null) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Schedules are not yet supported")); + } + + if (request.assigned() != null) + { + if (!userService.userExists(request.assigned())) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + } + + int taskId = taskService.createTask(request); + return ResponseEntity.status(HttpStatus.CREATED).body(new IdResponse(taskId)); + } + + @GetMapping("/tasks") + public List getTasks() + { + return taskService.getTasks(); + } + + @GetMapping("/task/{taskId}") + public ResponseEntity getTask(@PathVariable String taskId) + { + int id; + try + { + id = Integer.parseInt(taskId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } + + Optional task = taskService.getTask(id); + if (task.isEmpty()) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } + return ResponseEntity.ok(task.get()); + } + + @PutMapping("/task/{taskId}") + public ResponseEntity putTask(@PathVariable String taskId, @RequestBody CreateTaskRequest request) + { + int id; + try + { + id = Integer.parseInt(taskId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } + + Optional existing = taskService.getTask(id); + if (existing.isEmpty()) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } + + taskService.updateTask(id, request); + return ResponseEntity.ok(new MessageResponse("Task updated successfully")); + } + + @DeleteMapping("/task/{taskId}") + public ResponseEntity deleteTask(@PathVariable String taskId) + { + int id; + try + { + id = Integer.parseInt(taskId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } + + if (!taskService.hasTask(id)) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } + + taskService.deleteTask(id); + return ResponseEntity.ok(new MessageResponse("Task deleted successfully")); + } + + @PostMapping("/task/{taskId}/complete") + public ResponseEntity completeTask(@PathVariable String taskId) + { + int id; + try + { + id = Integer.parseInt(taskId); + } + catch (NumberFormatException e) + { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } + + boolean completed = taskService.completeTask(id); + if (!completed) + { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } + return ResponseEntity.ok(new MessageResponse("Task completed successfully")); + } + + // ---- Transfer ---- + + @PostMapping("/transfer") + public ResponseEntity transfer(@RequestBody TransferRequest request) + { + TransferService.TransferResult result = transferService.transfer(request); + return switch (result.status()) + { + case SUCCESS -> ResponseEntity.ok(new MessageResponse(result.message())); + case BAD_REQUEST -> ResponseEntity.badRequest().body(new ErrorResponse(result.message())); + case NOT_FOUND -> + ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(result.message())); + }; + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/WebController.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/WebController.java new file mode 100644 index 0000000..d290984 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/WebController.java @@ -0,0 +1,173 @@ +package be.seeseepuff.allowanceplanner.controller; + +import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.service.AllowanceService; +import be.seeseepuff.allowanceplanner.service.TaskService; +import be.seeseepuff.allowanceplanner.service.UserService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.Arrays; +import java.util.List; + +@Controller +public class WebController +{ + private final UserService userService; + private final AllowanceService allowanceService; + private final TaskService taskService; + + public WebController(UserService userService, AllowanceService allowanceService, TaskService taskService) + { + this.userService = userService; + this.allowanceService = allowanceService; + this.taskService = taskService; + } + + @GetMapping("/") + public String index(HttpServletRequest request, HttpServletResponse response, Model model) + { + Integer currentUser = getCurrentUser(request, response); + if (currentUser == null) + { + model.addAttribute("users", userService.getUsers()); + return "index"; + } + return renderWithUser(model, currentUser); + } + + @GetMapping("/login") + public String login(@RequestParam(required = false) String user, HttpServletResponse response) + { + if (user != null && !user.isEmpty()) + { + Cookie cookie = new Cookie("user", user); + cookie.setMaxAge(3600); + cookie.setHttpOnly(true); + response.addCookie(cookie); + } + return "redirect:/"; + } + + @PostMapping("/createTask") + public String createTask(@RequestParam String name, @RequestParam double reward, + @RequestParam(required = false) String schedule, + HttpServletRequest request, HttpServletResponse response, Model model) + { + Integer currentUser = getCurrentUser(request, response); + if (currentUser == null) + { + return "redirect:/"; + } + + if (name.isEmpty() || reward <= 0) + { + model.addAttribute("error", "Invalid input"); + return "index"; + } + + CreateTaskRequest taskRequest = new CreateTaskRequest(name, reward, null, + (schedule != null && !schedule.isEmpty()) ? schedule : null); + taskService.createTask(taskRequest); + return "redirect:/"; + } + + @GetMapping("/completeTask") + public String completeTask(@RequestParam("task") int taskId) + { + taskService.completeTask(taskId); + return "redirect:/"; + } + + @PostMapping("/createAllowance") + public String createAllowance(@RequestParam String name, @RequestParam double target, + @RequestParam double weight, + HttpServletRequest request, HttpServletResponse response, Model model) + { + Integer currentUser = getCurrentUser(request, response); + if (currentUser == null) + { + return "redirect:/"; + } + + if (name.isEmpty() || target <= 0 || weight <= 0) + { + model.addAttribute("error", "Invalid input"); + return "index"; + } + + allowanceService.createAllowance(currentUser, new CreateAllowanceRequest(name, target, weight, "")); + return "redirect:/"; + } + + @GetMapping("/completeAllowance") + public String completeAllowance(@RequestParam("allowance") int allowanceId, + HttpServletRequest request, HttpServletResponse response) + { + Integer currentUser = getCurrentUser(request, response); + if (currentUser == null) + { + return "redirect:/"; + } + allowanceService.completeAllowance(currentUser, allowanceId); + return "redirect:/"; + } + + private Integer getCurrentUser(HttpServletRequest request, HttpServletResponse response) + { + Cookie[] cookies = request.getCookies(); + if (cookies == null) + { + return null; + } + String userStr = Arrays.stream(cookies) + .filter(c -> "user".equals(c.getName())) + .map(Cookie::getValue) + .findFirst() + .orElse(null); + if (userStr == null) + { + return null; + } + try + { + int userId = Integer.parseInt(userStr); + if (!userService.userExists(userId)) + { + unsetUserCookie(response); + return null; + } + return userId; + } + catch (NumberFormatException e) + { + unsetUserCookie(response); + return null; + } + } + + private void unsetUserCookie(HttpServletResponse response) + { + Cookie cookie = new Cookie("user", ""); + cookie.setMaxAge(0); + cookie.setPath("/"); + cookie.setHttpOnly(true); + response.addCookie(cookie); + } + + private String renderWithUser(Model model, int currentUser) + { + model.addAttribute("users", userService.getUsers()); + model.addAttribute("currentUser", currentUser); + model.addAttribute("allowances", allowanceService.getUserAllowances(currentUser)); + model.addAttribute("tasks", taskService.getTasks()); + model.addAttribute("history", allowanceService.getHistory(currentUser)); + return "index"; + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AddAllowanceAmountRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AddAllowanceAmountRequest.java new file mode 100644 index 0000000..f1c467d --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AddAllowanceAmountRequest.java @@ -0,0 +1,5 @@ +package be.seeseepuff.allowanceplanner.dto; + +public record AddAllowanceAmountRequest(double amount, String description) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AllowanceDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AllowanceDto.java new file mode 100644 index 0000000..fc7b2e7 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AllowanceDto.java @@ -0,0 +1,8 @@ +package be.seeseepuff.allowanceplanner.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.ALWAYS) +public record AllowanceDto(int id, String name, double target, double progress, double weight, String colour) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/BulkUpdateAllowanceRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/BulkUpdateAllowanceRequest.java new file mode 100644 index 0000000..124a08c --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/BulkUpdateAllowanceRequest.java @@ -0,0 +1,5 @@ +package be.seeseepuff.allowanceplanner.dto; + +public record BulkUpdateAllowanceRequest(int id, double weight) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateAllowanceRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateAllowanceRequest.java new file mode 100644 index 0000000..ac4e8aa --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateAllowanceRequest.java @@ -0,0 +1,5 @@ +package be.seeseepuff.allowanceplanner.dto; + +public record CreateAllowanceRequest(String name, double target, double weight, String colour) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateTaskRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateTaskRequest.java new file mode 100644 index 0000000..e430354 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateTaskRequest.java @@ -0,0 +1,11 @@ +package be.seeseepuff.allowanceplanner.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record CreateTaskRequest( + String name, + Double reward, + Integer assigned, + String schedule) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/ErrorResponse.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/ErrorResponse.java new file mode 100644 index 0000000..c5763de --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/ErrorResponse.java @@ -0,0 +1,5 @@ +package be.seeseepuff.allowanceplanner.dto; + +public record ErrorResponse(String error) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/HistoryDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/HistoryDto.java new file mode 100644 index 0000000..7e404bd --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/HistoryDto.java @@ -0,0 +1,7 @@ +package be.seeseepuff.allowanceplanner.dto; + +import java.time.Instant; + +public record HistoryDto(double allowance, Instant timestamp, String description) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/IdResponse.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/IdResponse.java new file mode 100644 index 0000000..54b2bea --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/IdResponse.java @@ -0,0 +1,5 @@ +package be.seeseepuff.allowanceplanner.dto; + +public record IdResponse(int id) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MessageResponse.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MessageResponse.java new file mode 100644 index 0000000..2fef1f9 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MessageResponse.java @@ -0,0 +1,5 @@ +package be.seeseepuff.allowanceplanner.dto; + +public record MessageResponse(String message) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/PostHistoryRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/PostHistoryRequest.java new file mode 100644 index 0000000..abff1a5 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/PostHistoryRequest.java @@ -0,0 +1,5 @@ +package be.seeseepuff.allowanceplanner.dto; + +public record PostHistoryRequest(double allowance, String description) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TaskDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TaskDto.java new file mode 100644 index 0000000..168f10c --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TaskDto.java @@ -0,0 +1,8 @@ +package be.seeseepuff.allowanceplanner.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.ALWAYS) +public record TaskDto(int id, String name, double reward, Integer assigned, String schedule) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TransferRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TransferRequest.java new file mode 100644 index 0000000..f631e40 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TransferRequest.java @@ -0,0 +1,5 @@ +package be.seeseepuff.allowanceplanner.dto; + +public record TransferRequest(int from, int to, double amount) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UpdateAllowanceRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UpdateAllowanceRequest.java new file mode 100644 index 0000000..7b1cca8 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UpdateAllowanceRequest.java @@ -0,0 +1,5 @@ +package be.seeseepuff.allowanceplanner.dto; + +public record UpdateAllowanceRequest(String name, double target, double weight, String colour) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserDto.java new file mode 100644 index 0000000..18a0823 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserDto.java @@ -0,0 +1,5 @@ +package be.seeseepuff.allowanceplanner.dto; + +public record UserDto(int id, String name) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserWithAllowanceDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserWithAllowanceDto.java new file mode 100644 index 0000000..553ddc0 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserWithAllowanceDto.java @@ -0,0 +1,5 @@ +package be.seeseepuff.allowanceplanner.dto; + +public record UserWithAllowanceDto(int id, String name, double allowance) +{ +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java new file mode 100644 index 0000000..f562b25 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java @@ -0,0 +1,99 @@ +package be.seeseepuff.allowanceplanner.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "allowances") +public class Allowance +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(name = "user_id", nullable = false) + private int userId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private long target; + + @Column(nullable = false) + private long balance = 0; + + @Column(nullable = false) + private double weight; + + private Integer colour; + + public int getId() + { + return id; + } + + public void setId(int id) + { + this.id = id; + } + + public int getUserId() + { + return userId; + } + + public void setUserId(int userId) + { + this.userId = userId; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public long getTarget() + { + return target; + } + + public void setTarget(long target) + { + this.target = target; + } + + public long getBalance() + { + return balance; + } + + public void setBalance(long balance) + { + this.balance = balance; + } + + public double getWeight() + { + return weight; + } + + public void setWeight(double weight) + { + this.weight = weight; + } + + public Integer getColour() + { + return colour; + } + + public void setColour(Integer colour) + { + this.colour = colour; + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java new file mode 100644 index 0000000..520422b --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java @@ -0,0 +1,73 @@ +package be.seeseepuff.allowanceplanner.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "history") +public class History +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(name = "user_id", nullable = false) + private int userId; + + @Column(nullable = false) + private long timestamp; + + @Column(nullable = false) + private long amount; + + private String description; + + public int getId() + { + return id; + } + + public void setId(int id) + { + this.id = id; + } + + public int getUserId() + { + return userId; + } + + public void setUserId(int userId) + { + this.userId = userId; + } + + public long getTimestamp() + { + return timestamp; + } + + public void setTimestamp(long timestamp) + { + this.timestamp = timestamp; + } + + public long getAmount() + { + return amount; + } + + public void setAmount(long amount) + { + this.amount = amount; + } + + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java new file mode 100644 index 0000000..474c98e --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java @@ -0,0 +1,97 @@ +package be.seeseepuff.allowanceplanner.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "tasks") +public class Task +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private long reward; + + private Integer assigned; + + private String schedule; + + private Long completed; + + @Column(name = "next_run") + private Long nextRun; + + public int getId() + { + return id; + } + + public void setId(int id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public long getReward() + { + return reward; + } + + public void setReward(long reward) + { + this.reward = reward; + } + + public Integer getAssigned() + { + return assigned; + } + + public void setAssigned(Integer assigned) + { + this.assigned = assigned; + } + + public String getSchedule() + { + return schedule; + } + + public void setSchedule(String schedule) + { + this.schedule = schedule; + } + + public Long getCompleted() + { + return completed; + } + + public void setCompleted(Long completed) + { + this.completed = completed; + } + + public Long getNextRun() + { + return nextRun; + } + + public void setNextRun(Long nextRun) + { + this.nextRun = nextRun; + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java new file mode 100644 index 0000000..5b1b501 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java @@ -0,0 +1,61 @@ +package be.seeseepuff.allowanceplanner.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "users") +public class User +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private double weight = 10.0; + + @Column(nullable = false) + private long balance = 0; + + public int getId() + { + return id; + } + + public void setId(int id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public double getWeight() + { + return weight; + } + + public void setWeight(double weight) + { + this.weight = weight; + } + + public long getBalance() + { + return balance; + } + + public void setBalance(long balance) + { + this.balance = balance; + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/AllowanceRepository.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/AllowanceRepository.java new file mode 100644 index 0000000..ba79bfb --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/AllowanceRepository.java @@ -0,0 +1,27 @@ +package be.seeseepuff.allowanceplanner.repository; + +import be.seeseepuff.allowanceplanner.entity.Allowance; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface AllowanceRepository extends JpaRepository +{ + List findByUserIdOrderByIdAsc(int userId); + + Optional findByIdAndUserId(int id, int userId); + + int countByIdAndUserId(int id, int userId); + + void deleteByIdAndUserId(int id, int userId); + + @Query("SELECT a FROM Allowance a WHERE a.userId = :userId AND a.weight > 0 ORDER BY (a.target - a.balance) ASC") + List findByUserIdWithPositiveWeightOrderByRemainingAsc(int userId); + + @Query("SELECT COALESCE(SUM(a.weight), 0) FROM Allowance a WHERE a.userId = :userId AND a.weight > 0") + double sumPositiveWeights(int userId); +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/HistoryRepository.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/HistoryRepository.java new file mode 100644 index 0000000..67030fe --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/HistoryRepository.java @@ -0,0 +1,13 @@ +package be.seeseepuff.allowanceplanner.repository; + +import be.seeseepuff.allowanceplanner.entity.History; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface HistoryRepository extends JpaRepository +{ + List findByUserIdOrderByIdDesc(int userId); +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/TaskRepository.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/TaskRepository.java new file mode 100644 index 0000000..7e6fac0 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/TaskRepository.java @@ -0,0 +1,16 @@ +package be.seeseepuff.allowanceplanner.repository; + +import be.seeseepuff.allowanceplanner.entity.Task; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface TaskRepository extends JpaRepository +{ + List findByCompletedIsNull(); + + Optional findByIdAndCompletedIsNull(int id); +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/UserRepository.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/UserRepository.java new file mode 100644 index 0000000..edf7723 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/UserRepository.java @@ -0,0 +1,13 @@ +package be.seeseepuff.allowanceplanner.repository; + +import be.seeseepuff.allowanceplanner.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository +{ + @Query("SELECT COALESCE(SUM(h.amount), 0) FROM History h WHERE h.userId = :userId") + long sumHistoryAmount(int userId); +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/AllowanceService.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/AllowanceService.java new file mode 100644 index 0000000..0b3d6a7 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/AllowanceService.java @@ -0,0 +1,298 @@ +package be.seeseepuff.allowanceplanner.service; + +import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.entity.Allowance; +import be.seeseepuff.allowanceplanner.entity.History; +import be.seeseepuff.allowanceplanner.entity.User; +import be.seeseepuff.allowanceplanner.repository.AllowanceRepository; +import be.seeseepuff.allowanceplanner.repository.HistoryRepository; +import be.seeseepuff.allowanceplanner.repository.UserRepository; +import be.seeseepuff.allowanceplanner.util.ColourUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class AllowanceService +{ + private final AllowanceRepository allowanceRepository; + private final UserRepository userRepository; + private final HistoryRepository historyRepository; + + public AllowanceService(AllowanceRepository allowanceRepository, + UserRepository userRepository, + HistoryRepository historyRepository) + { + this.allowanceRepository = allowanceRepository; + this.userRepository = userRepository; + this.historyRepository = historyRepository; + } + + public List getUserAllowances(int userId) + { + User user = userRepository.findById(userId).orElseThrow(); + List result = new ArrayList<>(); + + // Add the "rest" allowance (id=0) + result.add(new AllowanceDto(0, "", 0, user.getBalance() / 100.0, user.getWeight(), "")); + + // Add named allowances + for (Allowance a : allowanceRepository.findByUserIdOrderByIdAsc(userId)) + { + result.add(toDto(a)); + } + return result; + } + + public Optional getUserAllowanceById(int userId, int allowanceId) + { + if (allowanceId == 0) + { + return userRepository.findById(userId) + .map(u -> new AllowanceDto(0, "", 0, u.getBalance() / 100.0, u.getWeight(), "")); + } + return allowanceRepository.findByIdAndUserId(allowanceId, userId) + .map(this::toDto); + } + + @Transactional + public int createAllowance(int userId, CreateAllowanceRequest request) + { + int colour = ColourUtil.convertStringToColour(request.colour()); + Allowance allowance = new Allowance(); + allowance.setUserId(userId); + allowance.setName(request.name()); + allowance.setTarget(Math.round(request.target() * 100.0)); + allowance.setWeight(request.weight()); + allowance.setColour(colour); + allowance = allowanceRepository.save(allowance); + return allowance.getId(); + } + + @Transactional + public boolean deleteAllowance(int userId, int allowanceId) + { + int count = allowanceRepository.countByIdAndUserId(allowanceId, userId); + if (count == 0) + { + return false; + } + allowanceRepository.deleteByIdAndUserId(allowanceId, userId); + return true; + } + + @Transactional + public boolean updateAllowance(int userId, int allowanceId, UpdateAllowanceRequest request) + { + if (allowanceId == 0) + { + User user = userRepository.findById(userId).orElseThrow(); + user.setWeight(request.weight()); + userRepository.save(user); + return true; + } + + Optional opt = allowanceRepository.findByIdAndUserId(allowanceId, userId); + if (opt.isEmpty()) + { + return false; + } + + int colour = ColourUtil.convertStringToColour(request.colour()); + Allowance allowance = opt.get(); + allowance.setName(request.name()); + allowance.setTarget(Math.round(request.target() * 100.0)); + allowance.setWeight(request.weight()); + allowance.setColour(colour); + allowanceRepository.save(allowance); + return true; + } + + @Transactional + public void bulkUpdateAllowance(int userId, List requests) + { + for (BulkUpdateAllowanceRequest req : requests) + { + if (req.id() == 0) + { + User user = userRepository.findById(userId).orElseThrow(); + user.setWeight(req.weight()); + userRepository.save(user); + } + else + { + allowanceRepository.findByIdAndUserId(req.id(), userId).ifPresent(a -> + { + a.setWeight(req.weight()); + allowanceRepository.save(a); + }); + } + } + } + + @Transactional + public boolean completeAllowance(int userId, int allowanceId) + { + Optional opt = allowanceRepository.findByIdAndUserId(allowanceId, userId); + if (opt.isEmpty()) + { + return false; + } + + Allowance allowance = opt.get(); + long cost = allowance.getBalance(); + String allowanceName = allowance.getName(); + + // Delete the allowance + allowanceRepository.delete(allowance); + + // Add a history entry + History history = new History(); + history.setUserId(userId); + history.setTimestamp(Instant.now().getEpochSecond()); + history.setAmount(-cost); + history.setDescription("Allowance completed: " + allowanceName); + historyRepository.save(history); + + return true; + } + + @Transactional + public boolean addAllowanceAmount(int userId, int allowanceId, AddAllowanceAmountRequest request) + { + long remainingAmount = Math.round(request.amount() * 100); + + // Insert history entry + History history = new History(); + history.setUserId(userId); + history.setTimestamp(Instant.now().getEpochSecond()); + history.setAmount(remainingAmount); + history.setDescription(request.description()); + historyRepository.save(history); + + if (allowanceId == 0) + { + if (remainingAmount < 0) + { + User user = userRepository.findById(userId).orElseThrow(); + if (remainingAmount > user.getBalance()) + { + throw new IllegalArgumentException("cannot remove more than the current balance: " + user.getBalance()); + } + } + User user = userRepository.findById(userId).orElseThrow(); + user.setBalance(user.getBalance() + remainingAmount); + userRepository.save(user); + } + else if (remainingAmount < 0) + { + Allowance allowance = allowanceRepository.findByIdAndUserId(allowanceId, userId).orElse(null); + if (allowance == null) + { + return false; + } + if (remainingAmount > allowance.getBalance()) + { + throw new IllegalArgumentException("cannot remove more than the current allowance balance: " + allowance.getBalance()); + } + allowance.setBalance(allowance.getBalance() + remainingAmount); + allowanceRepository.save(allowance); + } + else + { + Allowance allowance = allowanceRepository.findByIdAndUserId(allowanceId, userId).orElse(null); + if (allowance == null) + { + return false; + } + + long toAdd = remainingAmount; + if (allowance.getBalance() + toAdd > allowance.getTarget()) + { + toAdd = allowance.getTarget() - allowance.getBalance(); + } + remainingAmount -= toAdd; + + if (toAdd > 0) + { + allowance.setBalance(allowance.getBalance() + toAdd); + allowanceRepository.save(allowance); + } + + if (remainingAmount > 0) + { + addDistributedReward(userId, (int) remainingAmount); + } + } + return true; + } + + public void addDistributedReward(int userId, int reward) + { + User user = userRepository.findById(userId).orElseThrow(); + double userWeight = user.getWeight(); + + double sumOfWeights = allowanceRepository.sumPositiveWeights(userId) + userWeight; + + int remainingReward = reward; + + if (sumOfWeights > 0) + { + List allowances = allowanceRepository.findByUserIdWithPositiveWeightOrderByRemainingAsc(userId); + for (Allowance allowance : allowances) + { + int amount = (int) ((allowance.getWeight() / sumOfWeights) * remainingReward); + if (allowance.getBalance() + amount > allowance.getTarget()) + { + amount = (int) (allowance.getTarget() - allowance.getBalance()); + } + sumOfWeights -= allowance.getWeight(); + allowance.setBalance(allowance.getBalance() + amount); + allowanceRepository.save(allowance); + remainingReward -= amount; + } + } + + // Add remaining to user's balance + user = userRepository.findById(userId).orElseThrow(); + user.setBalance(user.getBalance() + remainingReward); + userRepository.save(user); + } + + public List getHistory(int userId) + { + return historyRepository.findByUserIdOrderByIdDesc(userId).stream() + .map(h -> new HistoryDto( + h.getAmount() / 100.0, + Instant.ofEpochSecond(h.getTimestamp()), + h.getDescription())) + .toList(); + } + + @Transactional + public void addHistory(int userId, PostHistoryRequest request) + { + long amount = Math.round(request.allowance() * 100.0); + History history = new History(); + history.setUserId(userId); + history.setTimestamp(Instant.now().getEpochSecond()); + history.setAmount(amount); + history.setDescription(request.description()); + historyRepository.save(history); + } + + private AllowanceDto toDto(Allowance a) + { + return new AllowanceDto( + a.getId(), + a.getName(), + a.getTarget() / 100.0, + a.getBalance() / 100.0, + a.getWeight(), + ColourUtil.convertColourToString(a.getColour())); + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TaskService.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TaskService.java new file mode 100644 index 0000000..47dd254 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TaskService.java @@ -0,0 +1,132 @@ +package be.seeseepuff.allowanceplanner.service; + +import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.entity.History; +import be.seeseepuff.allowanceplanner.entity.Task; +import be.seeseepuff.allowanceplanner.entity.User; +import be.seeseepuff.allowanceplanner.repository.HistoryRepository; +import be.seeseepuff.allowanceplanner.repository.TaskRepository; +import be.seeseepuff.allowanceplanner.repository.UserRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +@Service +public class TaskService +{ + private final TaskRepository taskRepository; + private final UserRepository userRepository; + private final HistoryRepository historyRepository; + private final AllowanceService allowanceService; + + public TaskService(TaskRepository taskRepository, + UserRepository userRepository, + HistoryRepository historyRepository, + AllowanceService allowanceService) + { + this.taskRepository = taskRepository; + this.userRepository = userRepository; + this.historyRepository = historyRepository; + this.allowanceService = allowanceService; + } + + @Transactional + public int createTask(CreateTaskRequest request) + { + Task task = new Task(); + task.setName(request.name()); + task.setReward(Math.round((request.reward() != null ? request.reward() : 0.0) * 100.0)); + task.setAssigned(request.assigned()); + task = taskRepository.save(task); + return task.getId(); + } + + public List getTasks() + { + return taskRepository.findByCompletedIsNull().stream() + .map(this::toDto) + .toList(); + } + + public Optional getTask(int taskId) + { + return taskRepository.findByIdAndCompletedIsNull(taskId) + .map(this::toDto); + } + + @Transactional + public boolean updateTask(int taskId, CreateTaskRequest request) + { + Optional opt = taskRepository.findByIdAndCompletedIsNull(taskId); + if (opt.isEmpty()) + { + return false; + } + Task task = opt.get(); + task.setName(request.name()); + task.setReward(Math.round((request.reward() != null ? request.reward() : 0.0) * 100.0)); + task.setAssigned(request.assigned()); + taskRepository.save(task); + return true; + } + + public boolean hasTask(int taskId) + { + return taskRepository.existsById(taskId); + } + + @Transactional + public void deleteTask(int taskId) + { + taskRepository.deleteById(taskId); + } + + @Transactional + public boolean completeTask(int taskId) + { + Optional opt = taskRepository.findById(taskId); + if (opt.isEmpty()) + { + return false; + } + + Task task = opt.get(); + long reward = task.getReward(); + String rewardName = task.getName(); + + // Give reward to all users + List users = userRepository.findAll(); + for (User user : users) + { + // Add history entry + History history = new History(); + history.setUserId(user.getId()); + history.setTimestamp(Instant.now().getEpochSecond()); + history.setAmount(reward); + history.setDescription("Task completed: " + rewardName); + historyRepository.save(history); + + // Distribute reward + allowanceService.addDistributedReward(user.getId(), (int) reward); + } + + // Mark task as completed + task.setCompleted(Instant.now().getEpochSecond()); + taskRepository.save(task); + + return true; + } + + private TaskDto toDto(Task t) + { + return new TaskDto( + t.getId(), + t.getName(), + t.getReward() / 100.0, + t.getAssigned(), + t.getSchedule()); + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TransferService.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TransferService.java new file mode 100644 index 0000000..7b81ba0 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TransferService.java @@ -0,0 +1,102 @@ +package be.seeseepuff.allowanceplanner.service; + +import be.seeseepuff.allowanceplanner.dto.TransferRequest; +import be.seeseepuff.allowanceplanner.entity.Allowance; +import be.seeseepuff.allowanceplanner.repository.AllowanceRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +public class TransferService +{ + private final AllowanceRepository allowanceRepository; + + public TransferService(AllowanceRepository allowanceRepository) + { + this.allowanceRepository = allowanceRepository; + } + + @Transactional + public TransferResult transfer(TransferRequest request) + { + if (request.from() == request.to()) + { + return TransferResult.success(); + } + + int amountCents = (int) Math.round(request.amount() * 100.0); + if (amountCents <= 0) + { + return TransferResult.badRequest("amount must be positive"); + } + + Optional fromOpt = allowanceRepository.findById(request.from()); + if (fromOpt.isEmpty()) + { + return TransferResult.notFound(); + } + + Optional toOpt = allowanceRepository.findById(request.to()); + if (toOpt.isEmpty()) + { + return TransferResult.notFound(); + } + + Allowance from = fromOpt.get(); + Allowance to = toOpt.get(); + + if (from.getUserId() != to.getUserId()) + { + return TransferResult.badRequest("Allowances do not belong to the same user"); + } + + long remainingTo = to.getTarget() - to.getBalance(); + if (remainingTo <= 0) + { + return TransferResult.badRequest("target already reached"); + } + + int transfer = amountCents; + if (transfer > remainingTo) + { + transfer = (int) remainingTo; + } + + if (from.getBalance() < transfer) + { + return TransferResult.badRequest("Insufficient funds in source allowance"); + } + + from.setBalance(from.getBalance() - transfer); + to.setBalance(to.getBalance() + transfer); + allowanceRepository.save(from); + allowanceRepository.save(to); + + return TransferResult.success(); + } + + public record TransferResult(Status status, String message) + { + public enum Status + { + SUCCESS, BAD_REQUEST, NOT_FOUND + } + + public static TransferResult success() + { + return new TransferResult(Status.SUCCESS, "Transfer successful"); + } + + public static TransferResult badRequest(String message) + { + return new TransferResult(Status.BAD_REQUEST, message); + } + + public static TransferResult notFound() + { + return new TransferResult(Status.NOT_FOUND, "Allowance not found"); + } + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/UserService.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/UserService.java new file mode 100644 index 0000000..ccc74cd --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/UserService.java @@ -0,0 +1,42 @@ +package be.seeseepuff.allowanceplanner.service; + +import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.entity.User; +import be.seeseepuff.allowanceplanner.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class UserService +{ + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) + { + this.userRepository = userRepository; + } + + public List getUsers() + { + return userRepository.findAll().stream() + .map(u -> new UserDto(u.getId(), u.getName())) + .toList(); + } + + public Optional getUser(int userId) + { + return userRepository.findById(userId) + .map(u -> + { + long totalAmount = userRepository.sumHistoryAmount(userId); + return new UserWithAllowanceDto(u.getId(), u.getName(), totalAmount / 100.0); + }); + } + + public boolean userExists(int userId) + { + return userRepository.existsById(userId); + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/util/ColourUtil.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/util/ColourUtil.java new file mode 100644 index 0000000..6fda514 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/util/ColourUtil.java @@ -0,0 +1,42 @@ +package be.seeseepuff.allowanceplanner.util; + +public class ColourUtil +{ + private ColourUtil() + { + } + + public static int convertStringToColour(String colourStr) + { + if (colourStr == null || colourStr.isEmpty()) + { + return 0xFF0000; // Default colour + } + if (colourStr.charAt(0) == '#') + { + colourStr = colourStr.substring(1); + } + if (colourStr.length() != 6 && colourStr.length() != 3) + { + throw new IllegalArgumentException("colour must be a valid hex string"); + } + int colour = Integer.parseInt(colourStr, 16); + if (colourStr.length() == 3) + { + int r = (colour & 0xF00) >> 8; + int g = (colour & 0x0F0) >> 4; + int b = (colour & 0x00F); + colour = (r << 20) | (g << 12) | (b << 4); + } + return colour; + } + + public static String convertColourToString(Integer colour) + { + if (colour == null) + { + return ""; + } + return String.format("#%06X", colour); + } +} diff --git a/backend-spring/src/main/resources/application.properties b/backend-spring/src/main/resources/application.properties new file mode 100644 index 0000000..35e78e6 --- /dev/null +++ b/backend-spring/src/main/resources/application.properties @@ -0,0 +1,12 @@ +spring.application.name=allowance-planner + +spring.datasource.url=jdbc:postgresql://localhost:5432/allowance_planner +spring.datasource.username=postgres +spring.datasource.password=postgres + +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.open-in-view=false + +spring.flyway.enabled=true + +server.port=8080 diff --git a/backend-spring/src/main/resources/db/migration/V1__initial.sql b/backend-spring/src/main/resources/db/migration/V1__initial.sql new file mode 100644 index 0000000..c956d03 --- /dev/null +++ b/backend-spring/src/main/resources/db/migration/V1__initial.sql @@ -0,0 +1,42 @@ +CREATE TABLE users +( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + weight DOUBLE PRECISION NOT NULL DEFAULT 10.0, + balance BIGINT NOT NULL DEFAULT 0 +); + +CREATE TABLE history +( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL, + timestamp BIGINT NOT NULL, + amount BIGINT NOT NULL, + description TEXT +); + +CREATE TABLE allowances +( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + target BIGINT NOT NULL, + balance BIGINT NOT NULL DEFAULT 0, + weight DOUBLE PRECISION NOT NULL, + colour INTEGER +); + +CREATE TABLE tasks +( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + reward BIGINT NOT NULL, + assigned INTEGER, + schedule TEXT, + completed BIGINT, + next_run BIGINT +); + +INSERT INTO users (name) +VALUES ('Seeseemelk'), + ('Huffle'); diff --git a/backend-spring/src/main/resources/templates/index.html b/backend-spring/src/main/resources/templates/index.html new file mode 100644 index 0000000..a199188 --- /dev/null +++ b/backend-spring/src/main/resources/templates/index.html @@ -0,0 +1,122 @@ + + + + Allowance Planner 2000 + + + +

Allowance Planner 2000

+ +
+

Error

+

+
+ +
+

Users

+ + + + + +
+

Allowances

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameProgressTargetWeightActions
Total + + () + + Mark as completed +
+
+ +

Tasks

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameAssignedRewardScheduleActions
+ None + + + Mark as completed +
+
+ +

History

+ + + + + + + + + + + + + +
TimestampAllowance
+
+
+ + diff --git a/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ApiTest.java b/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ApiTest.java new file mode 100644 index 0000000..1433976 --- /dev/null +++ b/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ApiTest.java @@ -0,0 +1,1242 @@ +package be.seeseepuff.allowanceplanner; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.flywaydb.core.Flyway; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Testcontainers +class ApiTest +{ + private static final String TEST_HISTORY_NAME = "Test History"; + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:17") + .withDatabaseName("allowance_planner_test") + .withUsername("test") + .withPassword("test"); + + @LocalServerPort + int port; + + @Autowired + Flyway flyway; + + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) + { + registry.add("spring.datasource.url", postgres::getJdbcUrl); + registry.add("spring.datasource.username", postgres::getUsername); + registry.add("spring.datasource.password", postgres::getPassword); + registry.add("spring.flyway.clean-disabled", () -> "false"); + } + + @BeforeEach + void setUp() + { + RestAssured.port = port; + RestAssured.basePath = "/api"; + RestAssured.config = RestAssured.config() + .jsonConfig(io.restassured.config.JsonConfig.jsonConfig() + .numberReturnType(io.restassured.path.json.config.JsonPathConfig.NumberReturnType.DOUBLE)); + + // Clean and re-migrate the database before each test + flyway.clean(); + flyway.migrate(); + } + + // ---- User Tests ---- + + @Test + void getUsers() + { + given() + .when() + .get("/users") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].name", isA(String.class)) + .body("[1].name", isA(String.class)); + } + + @Test + void getUser() + { + given() + .when() + .get("/user/1") + .then() + .statusCode(200) + .body("name", is("Seeseemelk")) + .body("id", is(1)) + .body("allowance", is(0.0d)); + } + + @Test + void getUserUnknown() + { + given() + .when() + .get("/user/999") + .then() + .statusCode(404); + } + + @Test + void getUserBadId() + { + given() + .when() + .get("/user/bad-id") + .then() + .statusCode(400); + } + + // ---- Allowance Tests ---- + + @Test + void getUserAllowanceWhenNoAllowancePresent() + { + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].id", is(0)); + } + + @Test + void getUserAllowance() + { + createAllowance(1, TEST_HISTORY_NAME, 5000, 10); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[1].name", is(TEST_HISTORY_NAME)) + .body("[1].target", is(5000.0d)) + .body("[1].weight", is(10.0d)) + .body("[1].progress", is(0.0d)) + .body("[1]", not(hasKey("user_id"))); + } + + @Test + void getUserAllowanceNoUser() + { + given() + .when() + .get("/user/999/allowance") + .then() + .statusCode(404); + } + + @Test + void getUserAllowanceBadId() + { + given() + .when() + .get("/user/bad-id/allowance") + .then() + .statusCode(400); + } + + @Test + void createUserAllowance() + { + int allowanceId = createAllowance(1, TEST_HISTORY_NAME, 5000, 10); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[1].id", is(allowanceId)) + .body("[1].name", is(TEST_HISTORY_NAME)) + .body("[1].target", is(5000.0d)) + .body("[1].weight", is(10.0d)) + .body("[1].progress", is(0.0d)); + } + + @Test + void createUserAllowanceNoUser() + { + given() + .contentType(ContentType.JSON) + .body(Map.of("name", TEST_HISTORY_NAME, "target", 5000, "weight", 10)) + .when() + .post("/user/999/allowance") + .then() + .statusCode(404); + } + + @Test + void createUserAllowanceInvalidInput() + { + // Empty name + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 5000, "weight", 10)) + .when() + .post("/user/1/allowance") + .then() + .statusCode(400); + + // Missing name + given() + .contentType(ContentType.JSON) + .body(Map.of("target", 5000)) + .when() + .post("/user/1/allowance") + .then() + .statusCode(400); + } + + @Test + void createUserAllowanceBadId() + { + given() + .contentType(ContentType.JSON) + .body(Map.of("name", TEST_HISTORY_NAME, "target", 5000, "weight", 10)) + .when() + .post("/user/bad-id/allowance") + .then() + .statusCode(400); + } + + @Test + void deleteUserAllowance() + { + int allowanceId = createAllowance(1, TEST_HISTORY_NAME, 1000, 5); + + given() + .when() + .delete("/user/1/allowance/" + allowanceId) + .then() + .statusCode(200) + .body("message", is("History deleted successfully")); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(1)); + } + + @Test + void deleteUserRestAllowance() + { + given() + .when() + .delete("/user/1/allowance/0") + .then() + .statusCode(400); + } + + @Test + void deleteUserAllowanceNotFound() + { + given() + .when() + .delete("/user/1/allowance/999") + .then() + .statusCode(404) + .body("error", is("History not found")); + } + + @Test + void deleteUserAllowanceInvalidId() + { + given() + .when() + .delete("/user/1/allowance/invalid-id") + .then() + .statusCode(400) + .body("error", is("Invalid allowance ID")); + } + + // ---- Task Tests ---- + + @Test + void createTask() + { + // Without assigned user + int taskId = given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Test Task", "reward", 100)) + .when() + .post("/tasks") + .then() + .statusCode(201) + .body("id", notNullValue()) + .extract().path("id"); + + given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); + + given() + .when() + .get("/task/" + taskId) + .then() + .statusCode(200) + .body("id", is(taskId)) + .body("name", is("Test Task")) + .body("reward", is(100.0d)) + .body("assigned", nullValue()); + + // With assigned user + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Test Task Assigned", "reward", 200, "assigned", 1)) + .when() + .post("/tasks") + .then() + .statusCode(201) + .body("id", notNullValue()); + } + + @Test + void deleteTask() + { + int taskId = createTestTask(100); + + given().when().delete("/task/" + taskId).then().statusCode(200); + given().when().get("/task/" + taskId).then().statusCode(404); + } + + @Test + void deleteTaskNotFound() + { + given().when().delete("/task/1").then().statusCode(404); + } + + @Test + void createTaskNoName() + { + given() + .contentType(ContentType.JSON) + .body(Map.of("reward", 100)) + .when() + .post("/tasks") + .then() + .statusCode(400); + } + + @Test + void createTaskInvalidAssignedUser() + { + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Test Task Invalid User", "reward", 100, "assigned", 999)) + .when() + .post("/tasks") + .then() + .statusCode(404) + .body("error", is("User not found")); + } + + @Test + void createTaskInvalidRequestBody() + { + given() + .contentType(ContentType.JSON) + .body(Map.of("reward", 5000)) + .when() + .post("/tasks") + .then() + .statusCode(400); + } + + @Test + void getTaskWhenNoTasks() + { + given() + .when() + .get("/tasks") + .then() + .statusCode(200) + .body("$.size()", is(0)); + } + + @Test + void getTasksWhenTasks() + { + createTestTask(100); + + given() + .when() + .get("/tasks") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].name", is("Test Task")) + .body("[0].reward", is(100.0d)) + .body("[0].assigned", nullValue()); + } + + @Test + void getTask() + { + int taskId = createTestTask(100); + + given() + .when() + .get("/task/" + taskId) + .then() + .statusCode(200) + .body("id", is(taskId)) + .body("name", is("Test Task")) + .body("reward", is(100.0d)) + .body("assigned", nullValue()); + } + + @Test + void getTaskInvalidId() + { + createTestTask(100); + // Task ID won't be found since we use auto-increment and there's only one + given().when().get("/task/99999").then().statusCode(404); + } + + @Test + void getTaskBadId() + { + createTestTask(100); + given().when().get("/task/invalid").then().statusCode(400); + } + + @Test + void putTaskModifiesTask() + { + int taskId = createTestTask(100); + + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Updated Task", "reward", 100)) + .when() + .put("/task/" + taskId) + .then() + .statusCode(200); + + given() + .when() + .get("/task/" + taskId) + .then() + .statusCode(200) + .body("id", is(taskId)) + .body("name", is("Updated Task")) + .body("reward", is(100.0d)); + } + + @Test + void putTaskInvalidTaskId() + { + createTestTask(100); + + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Updated Task")) + .when() + .put("/task/999") + .then() + .statusCode(404); + } + + // ---- History Tests ---- + + @Test + void postHistory() + { + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 100, "description", "Add a 100")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 20, "description", "Lolol")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", -10, "description", "Subtracting")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1") + .then() + .statusCode(200) + .body("allowance", is(110.0d)); + } + + @Test + void postHistoryInvalidUserId() + { + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 100, "description", "Good")) + .when() + .post("/user/999/history") + .then() + .statusCode(404); + } + + @Test + void postHistoryInvalidDescription() + { + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 100)) + .when() + .post("/user/1/history") + .then() + .statusCode(400); + } + + @Test + void getHistory() + { + Instant before = Instant.now().minusSeconds(2); + Instant after = Instant.now().plusSeconds(2); + + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 100, "description", "Add 100")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 20, "description", "Add 20")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", -10, "description", "Subtract 10")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + + // History is returned newest first (by ID desc) + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(3)) + .body("[0].allowance", is(-10.0d)) + .body("[0].description", is("Subtract 10")) + .body("[1].allowance", is(20.0d)) + .body("[1].description", is("Add 20")) + .body("[2].allowance", is(100.0d)) + .body("[2].description", is("Add 100")); + } + + // ---- Allowance By ID Tests ---- + + @Test + void getUserAllowanceById() + { + int allowanceId = createAllowanceWithColour(1, TEST_HISTORY_NAME, 5000, 10, "#FF5733"); + + given() + .when() + .get("/user/1/allowance/" + allowanceId) + .then() + .statusCode(200) + .body("id", is(allowanceId)) + .body("name", is(TEST_HISTORY_NAME)) + .body("target", is(5000.0d)) + .body("weight", is(10.0d)) + .body("progress", is(0.0d)) + .body("colour", is("#FF5733")); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[1].id", is(allowanceId)) + .body("[1].colour", is("#FF5733")); + } + + @Test + void getUserByAllowanceIdInvalidAllowance() + { + given().when().get("/user/1/allowance/9999").then().statusCode(404); + } + + @Test + void getUserByAllowanceByIdInvalidUserId() + { + given().when().get("/user/999/allowance/1").then().statusCode(404); + } + + @Test + void getUserByAllowanceByIdBadUserId() + { + given().when().get("/user/bad/allowance/1").then().statusCode(400); + } + + @Test + void getUserByAllowanceByIdBadAllowanceId() + { + given().when().get("/user/1/allowance/bad").then().statusCode(400); + } + + @Test + void putAllowanceById() + { + int allowanceId = createAllowanceWithColour(1, TEST_HISTORY_NAME, 5000, 10, "#FF5733"); + + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Updated Allowance", "target", 6000, "weight", 15, "colour", "#3357FF")) + .when() + .put("/user/1/allowance/" + allowanceId) + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance/" + allowanceId) + .then() + .statusCode(200) + .body("id", is(allowanceId)) + .body("name", is("Updated Allowance")) + .body("target", is(6000.0d)) + .body("weight", is(15.0d)) + .body("colour", is("#3357FF")); + } + + // ---- Complete Task Tests ---- + + @Test + void completeTask() + { + int taskId = createTestTask(101); + + given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); + + // Update rest allowance weight + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 0, "weight", 25, "colour", "")) + .when() + .put("/user/1/allowance/0") + .then() + .statusCode(200); + + // Create two allowance goals + createAllowance(1, "Test Allowance 1", 100, 50); + createAllowance(1, "Test Allowance 1", 10, 25); + + // Complete the task + given().when().post("/task/" + taskId + "/complete").then().statusCode(200); + + // Verify task is completed + given().when().get("/task/" + taskId).then().statusCode(404); + + // Verify allowances for user 1 + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(3)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(30.34, 0.01)) + .body("[1].progress", closeTo(60.66, 0.01)) + .body("[2].progress", closeTo(10.0, 0.01)); + + // Verify allowances for user 2 + given() + .when() + .get("/user/2/allowance") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(101.0, 0.01)); + + // Verify history for both users + for (int userId = 1; userId <= 2; userId++) + { + given() + .when() + .get("/user/" + userId + "/history") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].allowance", closeTo(101.0, 0.01)); + } + } + + @Test + void completeTaskWithNoWeights() + { + int taskId = createTestTask(101); + + given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); + + // Ensure main allowance has no weight + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 0, "weight", 0, "colour", "")) + .when() + .put("/user/1/allowance/0") + .then() + .statusCode(200); + + // Complete the task + given().when().post("/task/" + taskId + "/complete").then().statusCode(200); + + // Verify task is completed + given().when().get("/task/" + taskId).then().statusCode(404); + + // Verify allowances for user 1 + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(101.0, 0.01)); + + // Verify allowances for user 2 + given() + .when() + .get("/user/2/allowance") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(101.0, 0.01)); + } + + @Test + void completeTaskAllowanceWeightsSumTo0() + { + int taskId = createTestTask(101); + + given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); + + // Update rest allowance to 0 weight + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 0, "weight", 0, "colour", "")) + .when() + .put("/user/1/allowance/0") + .then() + .statusCode(200); + + // Create allowance with 0 weight + createAllowance(1, "Test Allowance 1", 1000, 0); + + // Complete the task + given().when().post("/task/" + taskId + "/complete").then().statusCode(200); + + // Verify allowances for user 1 + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(101.0, 0.01)) + .body("[1].progress", closeTo(0.0, 0.01)); + } + + @Test + void completeTaskInvalidId() + { + given().when().post("/task/999/complete").then().statusCode(404); + } + + // ---- Complete Allowance Tests ---- + + @Test + void completeAllowance() + { + createTestTask(100); + createAllowance(1, "Test Allowance 1", 100, 50); + + // Update base allowance to 0 weight + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 0, "weight", 0, "colour", "")) + .when() + .put("/user/1/allowance/0") + .then() + .statusCode(200); + + // Complete the task + given().when().post("/task/1/complete").then().statusCode(200); + + // Get the allowance ID (first named allowance) + int allowanceId = given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .extract() + .path("[1].id"); + + // Complete allowance + given().when().post("/user/1/allowance/" + allowanceId + "/complete").then().statusCode(200); + + // Verify allowance no longer exists + given().when().get("/user/1/allowance/" + allowanceId).then().statusCode(404); + + // Verify history (newest first) + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].allowance", closeTo(-100.0, 0.01)) + .body("[0].description", is("Allowance completed: Test Allowance 1")) + .body("[1].allowance", closeTo(100.0, 0.01)) + .body("[1].description", is("Task completed: Test Task")); + } + + @Test + void completeAllowanceInvalidUserId() + { + given().when().post("/user/999/allowance/1/complete").then().statusCode(404); + } + + @Test + void completeAllowanceInvalidAllowanceId() + { + given().when().post("/user/1/allowance/999/complete").then().statusCode(404); + } + + // ---- Bulk Update Tests ---- + + @Test + void putBulkAllowance() + { + int id1 = createAllowance(1, "Test Allowance 1", 1000, 1); + int id2 = createAllowance(1, "Test Allowance 2", 1000, 2); + + given() + .contentType(ContentType.JSON) + .body(List.of( + Map.of("id", id1, "weight", 5), + Map.of("id", 0, "weight", 99), + Map.of("id", id2, "weight", 10))) + .when() + .put("/user/1/allowance") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(3)) + .body("[0].id", is(0)) + .body("[0].weight", closeTo(99.0, 0.01)) + .body("[1].id", is(id1)) + .body("[1].weight", closeTo(5.0, 0.01)) + .body("[2].id", is(id2)) + .body("[2].weight", closeTo(10.0, 0.01)); + } + + // ---- Add Allowance Amount Tests ---- + + @Test + void addAllowanceSimple() + { + int allowanceId = createAllowance(1, "Test Allowance 1", 1000, 1); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/" + allowanceId + "/add") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[1].id", is(allowanceId)) + .body("[1].progress", closeTo(10.0, 0.01)); + + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].allowance", closeTo(10.0, 0.01)) + .body("[0].description", is("Added to allowance 1")); + } + + @Test + void addAllowanceWithSpillage() + { + int id1 = createAllowance(1, "Test Allowance 1", 5, 1); + int id2 = createAllowance(1, "Test Allowance 2", 5, 1); + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 0, "weight", 1, "colour", "")) + .when() + .put("/user/1/allowance/0") + .then() + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/" + id1 + "/add") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[1].id", is(id1)) + .body("[1].progress", closeTo(5.0, 0.01)) + .body("[2].id", is(id2)) + .body("[2].progress", closeTo(2.5, 0.01)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(2.5, 0.01)); + + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].allowance", closeTo(10.0, 0.01)) + .body("[0].description", is("Added to allowance 1")); + } + + @Test + void addAllowanceIdZero() + { + createAllowance(1, "Test Allowance 1", 1000, 1); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/0/add") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(10.0, 0.01)); + + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].allowance", closeTo(10.0, 0.01)) + .body("[0].description", is("Added to allowance 1")); + } + + @Test + void subtractAllowanceSimple() + { + int allowanceId = createAllowance(1, "Test Allowance 1", 1000, 1); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/" + allowanceId + "/add") + .then() + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", -2.5, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/" + allowanceId + "/add") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[1].id", is(allowanceId)) + .body("[1].progress", closeTo(7.5, 0.01)); + + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].allowance", closeTo(-2.5, 0.01)) + .body("[0].description", is("Added to allowance 1")) + .body("[1].allowance", closeTo(10.0, 0.01)) + .body("[1].description", is("Added to allowance 1")); + } + + @Test + void subtractAllowanceIdZero() + { + createAllowance(1, "Test Allowance 1", 1000, 1); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/0/add") + .then() + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", -2.5, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/0/add") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(7.5, 0.01)); + + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].allowance", closeTo(-2.5, 0.01)) + .body("[0].description", is("Added to allowance 1")) + .body("[1].allowance", closeTo(10.0, 0.01)) + .body("[1].description", is("Added to allowance 1")); + } + + // ---- Transfer Tests ---- + + @Test + void transferSuccessful() + { + int id1 = createAllowance(1, "From Allowance", 100, 1); + int id2 = createAllowance(1, "To Allowance", 100, 1); + + // Add 30 to allowance 1 + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 30, "description", "funds")) + .when() + .post("/user/1/allowance/" + id1 + "/add") + .then() + .statusCode(200); + + // Transfer 10 from 1 to 2 + given() + .contentType(ContentType.JSON) + .body(Map.of("from", id1, "to", id2, "amount", 10)) + .when() + .post("/transfer") + .then() + .statusCode(200) + .body("message", is("Transfer successful")); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[1].progress", closeTo(20.0, 0.01)) + .body("[2].progress", closeTo(10.0, 0.01)); + } + + @Test + void transferCapsAtTarget() + { + int id1 = createAllowance(1, "From Allowance", 100, 1); + int id2 = createAllowance(1, "To Allowance", 5, 1); + + // Add 10 to allowance 1 + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "funds")) + .when() + .post("/user/1/allowance/" + id1 + "/add") + .then() + .statusCode(200); + + // Transfer 10, but to only needs 5 + given() + .contentType(ContentType.JSON) + .body(Map.of("from", id1, "to", id2, "amount", 10)) + .when() + .post("/transfer") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[1].progress", closeTo(5.0, 0.01)) + .body("[2].progress", closeTo(5.0, 0.01)); + } + + @Test + void transferDifferentUsersFails() + { + int id1 = createAllowance(1, "User1 Allowance", 100, 1); + + // Create allowance for user 2 + int id2 = createAllowance(2, "User2 Allowance", 100, 1); + + // Add to user1 allowance + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "funds")) + .when() + .post("/user/1/allowance/" + id1 + "/add") + .then() + .statusCode(200); + + // Transfer between different users + given() + .contentType(ContentType.JSON) + .body(Map.of("from", id1, "to", id2, "amount", 5)) + .when() + .post("/transfer") + .then() + .statusCode(400); + } + + @Test + void transferInsufficientFunds() + { + int id1 = createAllowance(1, "From Allowance", 100, 1); + int id2 = createAllowance(1, "To Allowance", 100, 1); + + given() + .contentType(ContentType.JSON) + .body(Map.of("from", id1, "to", id2, "amount", 10)) + .when() + .post("/transfer") + .then() + .statusCode(400) + .body("error", containsStringIgnoringCase("insufficient")); + } + + @Test + void transferNotFound() + { + given() + .contentType(ContentType.JSON) + .body(Map.of("from", 999, "to", 1000, "amount", 1)) + .when() + .post("/transfer") + .then() + .statusCode(404); + } + + // ---- Helpers ---- + + private int createTestTask(int reward) + { + return given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Test Task", "reward", reward)) + .when() + .post("/tasks") + .then() + .statusCode(201) + .extract() + .path("id"); + } + + private int createAllowance(int userId, String name, double target, double weight) + { + return given() + .contentType(ContentType.JSON) + .body(Map.of("name", name, "target", target, "weight", weight)) + .when() + .post("/user/" + userId + "/allowance") + .then() + .statusCode(201) + .extract() + .path("id"); + } + + private int createAllowanceWithColour(int userId, String name, double target, double weight, String colour) + { + return given() + .contentType(ContentType.JSON) + .body(Map.of("name", name, "target", target, "weight", weight, "colour", colour)) + .when() + .post("/user/" + userId + "/allowance") + .then() + .statusCode(201) + .extract() + .path("id"); + } +} diff --git a/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ColourUtilTest.java b/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ColourUtilTest.java new file mode 100644 index 0000000..2e7766a --- /dev/null +++ b/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ColourUtilTest.java @@ -0,0 +1,33 @@ +package be.seeseepuff.allowanceplanner; + +import be.seeseepuff.allowanceplanner.util.ColourUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ColourUtilTest +{ + @Test + void convertStringToColourWithSign() + { + assertEquals(0x123456, ColourUtil.convertStringToColour("#123456")); + } + + @Test + void convertStringToColourWithoutSign() + { + assertEquals(0x123456, ColourUtil.convertStringToColour("123456")); + } + + @Test + void convertStringToColourWithSignThreeDigits() + { + assertEquals(0xA0B0C0, ColourUtil.convertStringToColour("#ABC")); + } + + @Test + void convertStringToColourWithoutSignThreeDigits() + { + assertEquals(0xA0B0C0, ColourUtil.convertStringToColour("ABC")); + } +} -- 2.49.1 From a08a462e2211991fd0ff02d49ec28a8095cc7b0f Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Sun, 1 Mar 2026 16:17:37 +0100 Subject: [PATCH 2/9] Add data migration export/import endpoints Add GET /api/export to the Go backend that dumps all users, allowances, history, and tasks (including completed) as a single JSON snapshot. Add POST /api/import to the Spring backend that accepts the same JSON, wipes existing data, inserts all records with original IDs preserved via native SQL, and resets PostgreSQL sequences to avoid future collisions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controller/ApiController.java | 15 ++- .../allowanceplanner/dto/MigrationDto.java | 21 ++++ .../service/MigrationService.java | 104 ++++++++++++++++++ backend/db.go | 56 ++++++++++ backend/dto.go | 42 +++++++ backend/main.go | 11 ++ 6 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MigrationDto.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/MigrationService.java diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java index 72dcb63..6f8e126 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java @@ -2,6 +2,7 @@ package be.seeseepuff.allowanceplanner.controller; import be.seeseepuff.allowanceplanner.dto.*; import be.seeseepuff.allowanceplanner.service.AllowanceService; +import be.seeseepuff.allowanceplanner.service.MigrationService; import be.seeseepuff.allowanceplanner.service.TaskService; import be.seeseepuff.allowanceplanner.service.TransferService; import be.seeseepuff.allowanceplanner.service.UserService; @@ -21,16 +22,19 @@ public class ApiController private final AllowanceService allowanceService; private final TaskService taskService; private final TransferService transferService; + private final MigrationService migrationService; public ApiController(UserService userService, AllowanceService allowanceService, TaskService taskService, - TransferService transferService) + TransferService transferService, + MigrationService migrationService) { this.userService = userService; this.allowanceService = allowanceService; this.taskService = taskService; this.transferService = transferService; + this.migrationService = migrationService; } // ---- Users ---- @@ -503,4 +507,13 @@ public class ApiController ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(result.message())); }; } + + // ---- Migration ---- + + @PostMapping("/import") + public ResponseEntity importData(@RequestBody MigrationDto data) + { + migrationService.importData(data); + return ResponseEntity.ok(new MessageResponse("Import successful")); + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MigrationDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MigrationDto.java new file mode 100644 index 0000000..cda8f22 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MigrationDto.java @@ -0,0 +1,21 @@ +package be.seeseepuff.allowanceplanner.dto; + +import java.util.List; + +public record MigrationDto( + List users, + List allowances, + List history, + List tasks +) +{ + public record MigrationUserDto(int id, String name, long balance, double weight) {} + + public record MigrationAllowanceDto(int id, int userId, String name, long target, long balance, double weight, + Integer colour) {} + + public record MigrationHistoryDto(int id, int userId, long timestamp, long amount, String description) {} + + public record MigrationTaskDto(int id, String name, long reward, Integer assigned, String schedule, + Long completed, Long nextRun) {} +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/MigrationService.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/MigrationService.java new file mode 100644 index 0000000..e717ffb --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/MigrationService.java @@ -0,0 +1,104 @@ +package be.seeseepuff.allowanceplanner.service; + +import be.seeseepuff.allowanceplanner.dto.MigrationDto; +import be.seeseepuff.allowanceplanner.repository.AllowanceRepository; +import be.seeseepuff.allowanceplanner.repository.HistoryRepository; +import be.seeseepuff.allowanceplanner.repository.TaskRepository; +import be.seeseepuff.allowanceplanner.repository.UserRepository; +import jakarta.persistence.EntityManager; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class MigrationService +{ + private final UserRepository userRepository; + private final AllowanceRepository allowanceRepository; + private final HistoryRepository historyRepository; + private final TaskRepository taskRepository; + private final EntityManager entityManager; + + public MigrationService(UserRepository userRepository, + AllowanceRepository allowanceRepository, + HistoryRepository historyRepository, + TaskRepository taskRepository, + EntityManager entityManager) + { + this.userRepository = userRepository; + this.allowanceRepository = allowanceRepository; + this.historyRepository = historyRepository; + this.taskRepository = taskRepository; + this.entityManager = entityManager; + } + + @Transactional + public void importData(MigrationDto data) + { + // Delete in dependency order + taskRepository.deleteAll(); + historyRepository.deleteAll(); + allowanceRepository.deleteAll(); + userRepository.deleteAll(); + + // Insert users with original IDs using native SQL to bypass auto-increment + for (MigrationDto.MigrationUserDto u : data.users()) + { + entityManager.createNativeQuery( + "INSERT INTO users (id, name, balance, weight) VALUES (:id, :name, :balance, :weight)") + .setParameter("id", u.id()) + .setParameter("name", u.name()) + .setParameter("balance", u.balance()) + .setParameter("weight", u.weight()) + .executeUpdate(); + } + + // Insert allowances with original IDs + for (MigrationDto.MigrationAllowanceDto a : data.allowances()) + { + entityManager.createNativeQuery( + "INSERT INTO allowances (id, user_id, name, target, balance, weight, colour) VALUES (:id, :userId, :name, :target, :balance, :weight, :colour)") + .setParameter("id", a.id()) + .setParameter("userId", a.userId()) + .setParameter("name", a.name()) + .setParameter("target", a.target()) + .setParameter("balance", a.balance()) + .setParameter("weight", a.weight()) + .setParameter("colour", a.colour()) + .executeUpdate(); + } + + // Insert history with original IDs + for (MigrationDto.MigrationHistoryDto h : data.history()) + { + entityManager.createNativeQuery( + "INSERT INTO history (id, user_id, timestamp, amount, description) VALUES (:id, :userId, :timestamp, :amount, :description)") + .setParameter("id", h.id()) + .setParameter("userId", h.userId()) + .setParameter("timestamp", h.timestamp()) + .setParameter("amount", h.amount()) + .setParameter("description", h.description()) + .executeUpdate(); + } + + // Insert tasks with original IDs + for (MigrationDto.MigrationTaskDto t : data.tasks()) + { + entityManager.createNativeQuery( + "INSERT INTO tasks (id, name, reward, assigned, schedule, completed, next_run) VALUES (:id, :name, :reward, :assigned, :schedule, :completed, :nextRun)") + .setParameter("id", t.id()) + .setParameter("name", t.name()) + .setParameter("reward", t.reward()) + .setParameter("assigned", t.assigned()) + .setParameter("schedule", t.schedule()) + .setParameter("completed", t.completed()) + .setParameter("nextRun", t.nextRun()) + .executeUpdate(); + } + + // Reset sequences so new inserts don't collide with migrated IDs + entityManager.createNativeQuery("SELECT setval('users_id_seq', COALESCE((SELECT MAX(id) FROM users), 0))").getSingleResult(); + entityManager.createNativeQuery("SELECT setval('allowances_id_seq', COALESCE((SELECT MAX(id) FROM allowances), 0))").getSingleResult(); + entityManager.createNativeQuery("SELECT setval('history_id_seq', COALESCE((SELECT MAX(id) FROM history), 0))").getSingleResult(); + entityManager.createNativeQuery("SELECT setval('tasks_id_seq', COALESCE((SELECT MAX(id) FROM tasks), 0))").getSingleResult(); + } +} diff --git a/backend/db.go b/backend/db.go index 46a0442..e6ac2a1 100644 --- a/backend/db.go +++ b/backend/db.go @@ -779,3 +779,59 @@ func (db *Db) TransferAllowance(fromId int, toId int, amount float64) error { return tx.Commit() } + +func (db *Db) ExportAllData() (*ExportData, error) { + var err error + data := &ExportData{ + Users: make([]ExportUser, 0), + Allowances: make([]ExportAllowance, 0), + History: make([]ExportHistory, 0), + Tasks: make([]ExportTask, 0), + } + + for row := range db.db.Query("select id, name, balance, weight from users").Range(&err) { + u := ExportUser{} + if err = row.Scan(&u.ID, &u.Name, &u.Balance, &u.Weight); err != nil { + return nil, err + } + data.Users = append(data.Users, u) + } + if err != nil { + return nil, err + } + + for row := range db.db.Query("select id, user_id, name, target, balance, weight, colour from allowances").Range(&err) { + a := ExportAllowance{} + if err = row.Scan(&a.ID, &a.UserID, &a.Name, &a.Target, &a.Balance, &a.Weight, &a.Colour); err != nil { + return nil, err + } + data.Allowances = append(data.Allowances, a) + } + if err != nil { + return nil, err + } + + for row := range db.db.Query("select id, user_id, timestamp, amount, description from history").Range(&err) { + h := ExportHistory{} + if err = row.Scan(&h.ID, &h.UserID, &h.Timestamp, &h.Amount, &h.Description); err != nil { + return nil, err + } + data.History = append(data.History, h) + } + if err != nil { + return nil, err + } + + for row := range db.db.Query("select id, name, reward, assigned, schedule, completed, next_run from tasks").Range(&err) { + t := ExportTask{} + if err = row.Scan(&t.ID, &t.Name, &t.Reward, &t.Assigned, &t.Schedule, &t.Completed, &t.NextRun); err != nil { + return nil, err + } + data.Tasks = append(data.Tasks, t) + } + if err != nil { + return nil, err + } + + return data, nil +} diff --git a/backend/dto.go b/backend/dto.go index bea039d..05d2014 100644 --- a/backend/dto.go +++ b/backend/dto.go @@ -86,3 +86,45 @@ type TransferRequest struct { To int `json:"to"` Amount float64 `json:"amount"` } + +type ExportUser struct { + ID int `json:"id"` + Name string `json:"name"` + Balance int64 `json:"balance"` + Weight float64 `json:"weight"` +} + +type ExportAllowance struct { + ID int `json:"id"` + UserID int `json:"userId"` + Name string `json:"name"` + Target int64 `json:"target"` + Balance int64 `json:"balance"` + Weight float64 `json:"weight"` + Colour *int `json:"colour"` +} + +type ExportHistory struct { + ID int `json:"id"` + UserID int `json:"userId"` + Timestamp int64 `json:"timestamp"` + Amount int64 `json:"amount"` + Description string `json:"description"` +} + +type ExportTask struct { + ID int `json:"id"` + Name string `json:"name"` + Reward int64 `json:"reward"` + Assigned *int `json:"assigned"` + Schedule *string `json:"schedule"` + Completed *int64 `json:"completed"` + NextRun *int64 `json:"nextRun"` +} + +type ExportData struct { + Users []ExportUser `json:"users"` + Allowances []ExportAllowance `json:"allowances"` + History []ExportHistory `json:"history"` + Tasks []ExportTask `json:"tasks"` +} diff --git a/backend/main.go b/backend/main.go index 132a4db..474aad6 100644 --- a/backend/main.go +++ b/backend/main.go @@ -51,6 +51,16 @@ const DefaultDomain = "localhost:8080" // The domain that the server is reachable at. var domain = DefaultDomain +func exportData(c *gin.Context) { + data, err := db.ExportAllData() + if err != nil { + log.Printf("Error exporting data: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) + return + } + c.IndentedJSON(http.StatusOK, data) +} + func getUsers(c *gin.Context) { users, err := db.GetUsers() if err != nil { @@ -713,6 +723,7 @@ func start(ctx context.Context, config *ServerConfig) { router.DELETE("/api/task/:taskId", deleteTask) router.POST("/api/task/:taskId/complete", completeTask) router.POST("/api/transfer", transfer) + router.GET("/api/export", exportData) srv := &http.Server{ Addr: config.Addr, -- 2.49.1 From e316d99453fcb67336657f14e687f2ae567463d9 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Sun, 1 Mar 2026 16:23:15 +0100 Subject: [PATCH 3/9] Reformat code --- .../AllowancePlannerApplication.java | 10 +- .../controller/ApiController.java | 768 +++--- .../controller/WebController.java | 258 +- .../dto/AddAllowanceAmountRequest.java | 3 +- .../allowanceplanner/dto/AllowanceDto.java | 3 +- .../dto/BulkUpdateAllowanceRequest.java | 3 +- .../dto/CreateAllowanceRequest.java | 3 +- .../dto/CreateTaskRequest.java | 11 +- .../allowanceplanner/dto/ErrorResponse.java | 3 +- .../allowanceplanner/dto/HistoryDto.java | 3 +- .../allowanceplanner/dto/IdResponse.java | 3 +- .../allowanceplanner/dto/MessageResponse.java | 3 +- .../allowanceplanner/dto/MigrationDto.java | 27 +- .../dto/PostHistoryRequest.java | 3 +- .../allowanceplanner/dto/TaskDto.java | 3 +- .../allowanceplanner/dto/TransferRequest.java | 3 +- .../dto/UpdateAllowanceRequest.java | 3 +- .../allowanceplanner/dto/UserDto.java | 3 +- .../dto/UserWithAllowanceDto.java | 3 +- .../allowanceplanner/entity/Allowance.java | 129 +- .../allowanceplanner/entity/History.java | 93 +- .../allowanceplanner/entity/Task.java | 125 +- .../allowanceplanner/entity/User.java | 77 +- .../repository/AllowanceRepository.java | 19 +- .../repository/HistoryRepository.java | 5 +- .../repository/TaskRepository.java | 7 +- .../repository/UserRepository.java | 7 +- .../service/AllowanceService.java | 456 ++-- .../service/MigrationService.java | 161 +- .../allowanceplanner/service/TaskService.java | 190 +- .../service/TransferService.java | 132 +- .../allowanceplanner/service/UserService.java | 51 +- .../allowanceplanner/util/ColourUtil.java | 65 +- .../src/main/resources/application.properties | 4 - .../src/main/resources/templates/index.html | 18 +- .../seeseepuff/allowanceplanner/ApiTest.java | 2373 ++++++++--------- .../allowanceplanner/ColourUtilTest.java | 39 +- 37 files changed, 2348 insertions(+), 2719 deletions(-) diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/AllowancePlannerApplication.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/AllowancePlannerApplication.java index dbc9452..b3e3c22 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/AllowancePlannerApplication.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/AllowancePlannerApplication.java @@ -4,10 +4,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class AllowancePlannerApplication -{ - public static void main(String[] args) - { - SpringApplication.run(AllowancePlannerApplication.class, args); - } +public class AllowancePlannerApplication { + static void main(String[] args) { + SpringApplication.run(AllowancePlannerApplication.class, args); + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java index 6f8e126..3c8d971 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java @@ -1,11 +1,7 @@ package be.seeseepuff.allowanceplanner.controller; import be.seeseepuff.allowanceplanner.dto.*; -import be.seeseepuff.allowanceplanner.service.AllowanceService; -import be.seeseepuff.allowanceplanner.service.MigrationService; -import be.seeseepuff.allowanceplanner.service.TaskService; -import be.seeseepuff.allowanceplanner.service.TransferService; -import be.seeseepuff.allowanceplanner.service.UserService; +import be.seeseepuff.allowanceplanner.service.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -16,504 +12,394 @@ import java.util.Optional; @RestController @RequestMapping("/api") @CrossOrigin(origins = "*") -public class ApiController -{ - private final UserService userService; - private final AllowanceService allowanceService; - private final TaskService taskService; - private final TransferService transferService; - private final MigrationService migrationService; +public class ApiController { + private final UserService userService; + private final AllowanceService allowanceService; + private final TaskService taskService; + private final TransferService transferService; + private final MigrationService migrationService; - public ApiController(UserService userService, - AllowanceService allowanceService, - TaskService taskService, - TransferService transferService, - MigrationService migrationService) - { - this.userService = userService; - this.allowanceService = allowanceService; - this.taskService = taskService; - this.transferService = transferService; - this.migrationService = migrationService; - } + public ApiController(UserService userService, + AllowanceService allowanceService, + TaskService taskService, + TransferService transferService, + MigrationService migrationService) { + this.userService = userService; + this.allowanceService = allowanceService; + this.taskService = taskService; + this.transferService = transferService; + this.migrationService = migrationService; + } - // ---- Users ---- + // ---- Users ---- - @GetMapping("/users") - public List getUsers() - { - return userService.getUsers(); - } + @GetMapping("/users") + public List getUsers() { + return userService.getUsers(); + } - @GetMapping("/user/{userId}") - public ResponseEntity getUser(@PathVariable String userId) - { - int id; - try - { - id = Integer.parseInt(userId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } + @GetMapping("/user/{userId}") + public ResponseEntity getUser(@PathVariable String userId) { + int id; + try { + id = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } - Optional user = userService.getUser(id); - if (user.isEmpty()) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } - return ResponseEntity.ok(user.get()); - } + Optional user = userService.getUser(id); + if (user.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + return ResponseEntity.ok(user.get()); + } - // ---- History ---- + // ---- History ---- - @PostMapping("/user/{userId}/history") - public ResponseEntity postHistory(@PathVariable String userId, @RequestBody PostHistoryRequest request) - { - int id; - try - { - id = Integer.parseInt(userId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } + @PostMapping("/user/{userId}/history") + public ResponseEntity postHistory(@PathVariable String userId, @RequestBody PostHistoryRequest request) { + int id; + try { + id = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } - if (request.description() == null || request.description().isEmpty()) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Description cannot be empty")); - } + if (request.description() == null || request.description().isEmpty()) { + return ResponseEntity.badRequest().body(new ErrorResponse("Description cannot be empty")); + } - if (!userService.userExists(id)) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } + if (!userService.userExists(id)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } - allowanceService.addHistory(id, request); - return ResponseEntity.ok(new MessageResponse("History updated successfully")); - } + allowanceService.addHistory(id, request); + return ResponseEntity.ok(new MessageResponse("History updated successfully")); + } - @GetMapping("/user/{userId}/history") - public ResponseEntity getHistory(@PathVariable String userId) - { - int id; - try - { - id = Integer.parseInt(userId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } + @GetMapping("/user/{userId}/history") + public ResponseEntity getHistory(@PathVariable String userId) { + int id; + try { + id = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } - List history = allowanceService.getHistory(id); - return ResponseEntity.ok(history); - } + List history = allowanceService.getHistory(id); + return ResponseEntity.ok(history); + } - // ---- Allowances ---- + // ---- Allowances ---- - @GetMapping("/user/{userId}/allowance") - public ResponseEntity getUserAllowance(@PathVariable String userId) - { - int id; - try - { - id = Integer.parseInt(userId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } + @GetMapping("/user/{userId}/allowance") + public ResponseEntity getUserAllowance(@PathVariable String userId) { + int id; + try { + id = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } - if (!userService.userExists(id)) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } + if (!userService.userExists(id)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } - return ResponseEntity.ok(allowanceService.getUserAllowances(id)); - } + return ResponseEntity.ok(allowanceService.getUserAllowances(id)); + } - @PostMapping("/user/{userId}/allowance") - public ResponseEntity createUserAllowance(@PathVariable String userId, - @RequestBody CreateAllowanceRequest request) - { - int id; - try - { - id = Integer.parseInt(userId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } + @PostMapping("/user/{userId}/allowance") + public ResponseEntity createUserAllowance(@PathVariable String userId, + @RequestBody CreateAllowanceRequest request) { + int id; + try { + id = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } - if (request.name() == null || request.name().isEmpty()) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Allowance name cannot be empty")); - } + if (request.name() == null || request.name().isEmpty()) { + return ResponseEntity.badRequest().body(new ErrorResponse("Allowance name cannot be empty")); + } - if (!userService.userExists(id)) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } + if (!userService.userExists(id)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } - int allowanceId = allowanceService.createAllowance(id, request); - return ResponseEntity.status(HttpStatus.CREATED).body(new IdResponse(allowanceId)); - } + int allowanceId = allowanceService.createAllowance(id, request); + return ResponseEntity.status(HttpStatus.CREATED).body(new IdResponse(allowanceId)); + } - @PutMapping("/user/{userId}/allowance") - public ResponseEntity bulkPutUserAllowance(@PathVariable String userId, - @RequestBody List requests) - { - int id; - try - { - id = Integer.parseInt(userId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } + @PutMapping("/user/{userId}/allowance") + public ResponseEntity bulkPutUserAllowance(@PathVariable String userId, + @RequestBody List requests) { + int id; + try { + id = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } - if (!userService.userExists(id)) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } + if (!userService.userExists(id)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } - allowanceService.bulkUpdateAllowance(id, requests); - return ResponseEntity.ok(new MessageResponse("Allowance updated successfully")); - } + allowanceService.bulkUpdateAllowance(id, requests); + return ResponseEntity.ok(new MessageResponse("Allowance updated successfully")); + } - @GetMapping("/user/{userId}/allowance/{allowanceId}") - public ResponseEntity getUserAllowanceById(@PathVariable String userId, @PathVariable String allowanceId) - { - int uid; - try - { - uid = Integer.parseInt(userId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } + @GetMapping("/user/{userId}/allowance/{allowanceId}") + public ResponseEntity getUserAllowanceById(@PathVariable String userId, @PathVariable String allowanceId) { + int uid; + try { + uid = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } - int aid; - try - { - aid = Integer.parseInt(allowanceId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); - } + int aid; + try { + aid = Integer.parseInt(allowanceId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); + } - if (!userService.userExists(uid)) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } + if (!userService.userExists(uid)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } - Optional allowance = allowanceService.getUserAllowanceById(uid, aid); - if (allowance.isEmpty()) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); - } - return ResponseEntity.ok(allowance.get()); - } + Optional allowance = allowanceService.getUserAllowanceById(uid, aid); + if (allowance.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); + } + return ResponseEntity.ok(allowance.get()); + } - @DeleteMapping("/user/{userId}/allowance/{allowanceId}") - public ResponseEntity deleteUserAllowance(@PathVariable String userId, @PathVariable String allowanceId) - { - int uid; - try - { - uid = Integer.parseInt(userId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } + @DeleteMapping("/user/{userId}/allowance/{allowanceId}") + public ResponseEntity deleteUserAllowance(@PathVariable String userId, @PathVariable String allowanceId) { + int uid; + try { + uid = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } - int aid; - try - { - aid = Integer.parseInt(allowanceId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); - } + int aid; + try { + aid = Integer.parseInt(allowanceId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); + } - if (aid == 0) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Allowance id zero cannot be deleted")); - } + if (aid == 0) { + return ResponseEntity.badRequest().body(new ErrorResponse("Allowance id zero cannot be deleted")); + } - if (!userService.userExists(uid)) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } + if (!userService.userExists(uid)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } - boolean deleted = allowanceService.deleteAllowance(uid, aid); - if (!deleted) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("History not found")); - } - return ResponseEntity.ok(new MessageResponse("History deleted successfully")); - } + boolean deleted = allowanceService.deleteAllowance(uid, aid); + if (!deleted) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("History not found")); + } + return ResponseEntity.ok(new MessageResponse("History deleted successfully")); + } - @PutMapping("/user/{userId}/allowance/{allowanceId}") - public ResponseEntity putUserAllowance(@PathVariable String userId, @PathVariable String allowanceId, - @RequestBody UpdateAllowanceRequest request) - { - int uid; - try - { - uid = Integer.parseInt(userId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } + @PutMapping("/user/{userId}/allowance/{allowanceId}") + public ResponseEntity putUserAllowance(@PathVariable String userId, @PathVariable String allowanceId, + @RequestBody UpdateAllowanceRequest request) { + int uid; + try { + uid = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } - int aid; - try - { - aid = Integer.parseInt(allowanceId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); - } + int aid; + try { + aid = Integer.parseInt(allowanceId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); + } - if (!userService.userExists(uid)) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } + if (!userService.userExists(uid)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } - boolean updated = allowanceService.updateAllowance(uid, aid, request); - if (!updated) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); - } - return ResponseEntity.ok(new MessageResponse("Allowance updated successfully")); - } + boolean updated = allowanceService.updateAllowance(uid, aid, request); + if (!updated) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); + } + return ResponseEntity.ok(new MessageResponse("Allowance updated successfully")); + } - @PostMapping("/user/{userId}/allowance/{allowanceId}/complete") - public ResponseEntity completeAllowance(@PathVariable String userId, @PathVariable String allowanceId) - { - int uid; - try - { - uid = Integer.parseInt(userId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } + @PostMapping("/user/{userId}/allowance/{allowanceId}/complete") + public ResponseEntity completeAllowance(@PathVariable String userId, @PathVariable String allowanceId) { + int uid; + try { + uid = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } - int aid; - try - { - aid = Integer.parseInt(allowanceId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); - } + int aid; + try { + aid = Integer.parseInt(allowanceId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); + } - if (!userService.userExists(uid)) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } + if (!userService.userExists(uid)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } - boolean completed = allowanceService.completeAllowance(uid, aid); - if (!completed) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); - } - return ResponseEntity.ok(new MessageResponse("Allowance completed successfully")); - } + boolean completed = allowanceService.completeAllowance(uid, aid); + if (!completed) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); + } + return ResponseEntity.ok(new MessageResponse("Allowance completed successfully")); + } - @PostMapping("/user/{userId}/allowance/{allowanceId}/add") - public ResponseEntity addToAllowance(@PathVariable String userId, @PathVariable String allowanceId, - @RequestBody AddAllowanceAmountRequest request) - { - int uid; - try - { - uid = Integer.parseInt(userId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } + @PostMapping("/user/{userId}/allowance/{allowanceId}/add") + public ResponseEntity addToAllowance(@PathVariable String userId, @PathVariable String allowanceId, + @RequestBody AddAllowanceAmountRequest request) { + int uid; + try { + uid = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } - int aid; - try - { - aid = Integer.parseInt(allowanceId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); - } + int aid; + try { + aid = Integer.parseInt(allowanceId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid allowance ID")); + } - if (!userService.userExists(uid)) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } + if (!userService.userExists(uid)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } - boolean result = allowanceService.addAllowanceAmount(uid, aid, request); - if (!result) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); - } - return ResponseEntity.ok(new MessageResponse("Allowance completed successfully")); - } + boolean result = allowanceService.addAllowanceAmount(uid, aid, request); + if (!result) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Allowance not found")); + } + return ResponseEntity.ok(new MessageResponse("Allowance completed successfully")); + } - // ---- Tasks ---- + // ---- Tasks ---- - @PostMapping("/tasks") - public ResponseEntity createTask(@RequestBody CreateTaskRequest request) - { - if (request.name() == null || request.name().isEmpty()) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Task name cannot be empty")); - } + @PostMapping("/tasks") + public ResponseEntity createTask(@RequestBody CreateTaskRequest request) { + if (request.name() == null || request.name().isEmpty()) { + return ResponseEntity.badRequest().body(new ErrorResponse("Task name cannot be empty")); + } - if (request.schedule() != null) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Schedules are not yet supported")); - } + if (request.schedule() != null) { + return ResponseEntity.badRequest().body(new ErrorResponse("Schedules are not yet supported")); + } - if (request.assigned() != null) - { - if (!userService.userExists(request.assigned())) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } - } + if (request.assigned() != null) { + if (!userService.userExists(request.assigned())) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + } - int taskId = taskService.createTask(request); - return ResponseEntity.status(HttpStatus.CREATED).body(new IdResponse(taskId)); - } + int taskId = taskService.createTask(request); + return ResponseEntity.status(HttpStatus.CREATED).body(new IdResponse(taskId)); + } - @GetMapping("/tasks") - public List getTasks() - { - return taskService.getTasks(); - } + @GetMapping("/tasks") + public List getTasks() { + return taskService.getTasks(); + } - @GetMapping("/task/{taskId}") - public ResponseEntity getTask(@PathVariable String taskId) - { - int id; - try - { - id = Integer.parseInt(taskId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); - } + @GetMapping("/task/{taskId}") + public ResponseEntity getTask(@PathVariable String taskId) { + int id; + try { + id = Integer.parseInt(taskId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } - Optional task = taskService.getTask(id); - if (task.isEmpty()) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); - } - return ResponseEntity.ok(task.get()); - } + Optional task = taskService.getTask(id); + if (task.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } + return ResponseEntity.ok(task.get()); + } - @PutMapping("/task/{taskId}") - public ResponseEntity putTask(@PathVariable String taskId, @RequestBody CreateTaskRequest request) - { - int id; - try - { - id = Integer.parseInt(taskId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); - } + @PutMapping("/task/{taskId}") + public ResponseEntity putTask(@PathVariable String taskId, @RequestBody CreateTaskRequest request) { + int id; + try { + id = Integer.parseInt(taskId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } - Optional existing = taskService.getTask(id); - if (existing.isEmpty()) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); - } + Optional existing = taskService.getTask(id); + if (existing.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } - taskService.updateTask(id, request); - return ResponseEntity.ok(new MessageResponse("Task updated successfully")); - } + taskService.updateTask(id, request); + return ResponseEntity.ok(new MessageResponse("Task updated successfully")); + } - @DeleteMapping("/task/{taskId}") - public ResponseEntity deleteTask(@PathVariable String taskId) - { - int id; - try - { - id = Integer.parseInt(taskId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); - } + @DeleteMapping("/task/{taskId}") + public ResponseEntity deleteTask(@PathVariable String taskId) { + int id; + try { + id = Integer.parseInt(taskId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } - if (!taskService.hasTask(id)) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); - } + if (!taskService.hasTask(id)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } - taskService.deleteTask(id); - return ResponseEntity.ok(new MessageResponse("Task deleted successfully")); - } + taskService.deleteTask(id); + return ResponseEntity.ok(new MessageResponse("Task deleted successfully")); + } - @PostMapping("/task/{taskId}/complete") - public ResponseEntity completeTask(@PathVariable String taskId) - { - int id; - try - { - id = Integer.parseInt(taskId); - } - catch (NumberFormatException e) - { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); - } + @PostMapping("/task/{taskId}/complete") + public ResponseEntity completeTask(@PathVariable String taskId) { + int id; + try { + id = Integer.parseInt(taskId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } - boolean completed = taskService.completeTask(id); - if (!completed) - { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); - } - return ResponseEntity.ok(new MessageResponse("Task completed successfully")); - } + boolean completed = taskService.completeTask(id); + if (!completed) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } + return ResponseEntity.ok(new MessageResponse("Task completed successfully")); + } - // ---- Transfer ---- + // ---- Transfer ---- - @PostMapping("/transfer") - public ResponseEntity transfer(@RequestBody TransferRequest request) - { - TransferService.TransferResult result = transferService.transfer(request); - return switch (result.status()) - { - case SUCCESS -> ResponseEntity.ok(new MessageResponse(result.message())); - case BAD_REQUEST -> ResponseEntity.badRequest().body(new ErrorResponse(result.message())); - case NOT_FOUND -> - ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(result.message())); - }; - } + @PostMapping("/transfer") + public ResponseEntity transfer(@RequestBody TransferRequest request) { + TransferService.TransferResult result = transferService.transfer(request); + return switch (result.status()) { + case SUCCESS -> ResponseEntity.ok(new MessageResponse(result.message())); + case BAD_REQUEST -> ResponseEntity.badRequest().body(new ErrorResponse(result.message())); + case NOT_FOUND -> ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(result.message())); + }; + } - // ---- Migration ---- + // ---- Migration ---- - @PostMapping("/import") - public ResponseEntity importData(@RequestBody MigrationDto data) - { - migrationService.importData(data); - return ResponseEntity.ok(new MessageResponse("Import successful")); - } + @PostMapping("/import") + public ResponseEntity importData(@RequestBody MigrationDto data) { + migrationService.importData(data); + return ResponseEntity.ok(new MessageResponse("Import successful")); + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/WebController.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/WebController.java index d290984..9e4bc78 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/WebController.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/WebController.java @@ -1,6 +1,7 @@ package be.seeseepuff.allowanceplanner.controller; -import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.dto.CreateAllowanceRequest; +import be.seeseepuff.allowanceplanner.dto.CreateTaskRequest; import be.seeseepuff.allowanceplanner.service.AllowanceService; import be.seeseepuff.allowanceplanner.service.TaskService; import be.seeseepuff.allowanceplanner.service.UserService; @@ -14,160 +15,135 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.Arrays; -import java.util.List; @Controller -public class WebController -{ - private final UserService userService; - private final AllowanceService allowanceService; - private final TaskService taskService; +public class WebController { + private final UserService userService; + private final AllowanceService allowanceService; + private final TaskService taskService; - public WebController(UserService userService, AllowanceService allowanceService, TaskService taskService) - { - this.userService = userService; - this.allowanceService = allowanceService; - this.taskService = taskService; - } + public WebController(UserService userService, AllowanceService allowanceService, TaskService taskService) { + this.userService = userService; + this.allowanceService = allowanceService; + this.taskService = taskService; + } - @GetMapping("/") - public String index(HttpServletRequest request, HttpServletResponse response, Model model) - { - Integer currentUser = getCurrentUser(request, response); - if (currentUser == null) - { - model.addAttribute("users", userService.getUsers()); - return "index"; - } - return renderWithUser(model, currentUser); - } + @GetMapping("/") + public String index(HttpServletRequest request, HttpServletResponse response, Model model) { + Integer currentUser = getCurrentUser(request, response); + if (currentUser == null) { + model.addAttribute("users", userService.getUsers()); + return "index"; + } + return renderWithUser(model, currentUser); + } - @GetMapping("/login") - public String login(@RequestParam(required = false) String user, HttpServletResponse response) - { - if (user != null && !user.isEmpty()) - { - Cookie cookie = new Cookie("user", user); - cookie.setMaxAge(3600); - cookie.setHttpOnly(true); - response.addCookie(cookie); - } - return "redirect:/"; - } + @GetMapping("/login") + public String login(@RequestParam(required = false) String user, HttpServletResponse response) { + if (user != null && !user.isEmpty()) { + Cookie cookie = new Cookie("user", user); + cookie.setMaxAge(3600); + cookie.setHttpOnly(true); + response.addCookie(cookie); + } + return "redirect:/"; + } - @PostMapping("/createTask") - public String createTask(@RequestParam String name, @RequestParam double reward, - @RequestParam(required = false) String schedule, - HttpServletRequest request, HttpServletResponse response, Model model) - { - Integer currentUser = getCurrentUser(request, response); - if (currentUser == null) - { - return "redirect:/"; - } + @PostMapping("/createTask") + public String createTask(@RequestParam String name, @RequestParam double reward, + @RequestParam(required = false) String schedule, + HttpServletRequest request, HttpServletResponse response, Model model) { + Integer currentUser = getCurrentUser(request, response); + if (currentUser == null) { + return "redirect:/"; + } - if (name.isEmpty() || reward <= 0) - { - model.addAttribute("error", "Invalid input"); - return "index"; - } + if (name.isEmpty() || reward <= 0) { + model.addAttribute("error", "Invalid input"); + return "index"; + } - CreateTaskRequest taskRequest = new CreateTaskRequest(name, reward, null, - (schedule != null && !schedule.isEmpty()) ? schedule : null); - taskService.createTask(taskRequest); - return "redirect:/"; - } + CreateTaskRequest taskRequest = new CreateTaskRequest(name, reward, null, + (schedule != null && !schedule.isEmpty()) ? schedule : null); + taskService.createTask(taskRequest); + return "redirect:/"; + } - @GetMapping("/completeTask") - public String completeTask(@RequestParam("task") int taskId) - { - taskService.completeTask(taskId); - return "redirect:/"; - } + @GetMapping("/completeTask") + public String completeTask(@RequestParam("task") int taskId) { + taskService.completeTask(taskId); + return "redirect:/"; + } - @PostMapping("/createAllowance") - public String createAllowance(@RequestParam String name, @RequestParam double target, - @RequestParam double weight, - HttpServletRequest request, HttpServletResponse response, Model model) - { - Integer currentUser = getCurrentUser(request, response); - if (currentUser == null) - { - return "redirect:/"; - } + @PostMapping("/createAllowance") + public String createAllowance(@RequestParam String name, @RequestParam double target, + @RequestParam double weight, + HttpServletRequest request, HttpServletResponse response, Model model) { + Integer currentUser = getCurrentUser(request, response); + if (currentUser == null) { + return "redirect:/"; + } - if (name.isEmpty() || target <= 0 || weight <= 0) - { - model.addAttribute("error", "Invalid input"); - return "index"; - } + if (name.isEmpty() || target <= 0 || weight <= 0) { + model.addAttribute("error", "Invalid input"); + return "index"; + } - allowanceService.createAllowance(currentUser, new CreateAllowanceRequest(name, target, weight, "")); - return "redirect:/"; - } + allowanceService.createAllowance(currentUser, new CreateAllowanceRequest(name, target, weight, "")); + return "redirect:/"; + } - @GetMapping("/completeAllowance") - public String completeAllowance(@RequestParam("allowance") int allowanceId, - HttpServletRequest request, HttpServletResponse response) - { - Integer currentUser = getCurrentUser(request, response); - if (currentUser == null) - { - return "redirect:/"; - } - allowanceService.completeAllowance(currentUser, allowanceId); - return "redirect:/"; - } + @GetMapping("/completeAllowance") + public String completeAllowance(@RequestParam("allowance") int allowanceId, + HttpServletRequest request, HttpServletResponse response) { + Integer currentUser = getCurrentUser(request, response); + if (currentUser == null) { + return "redirect:/"; + } + allowanceService.completeAllowance(currentUser, allowanceId); + return "redirect:/"; + } - private Integer getCurrentUser(HttpServletRequest request, HttpServletResponse response) - { - Cookie[] cookies = request.getCookies(); - if (cookies == null) - { - return null; - } - String userStr = Arrays.stream(cookies) - .filter(c -> "user".equals(c.getName())) - .map(Cookie::getValue) - .findFirst() - .orElse(null); - if (userStr == null) - { - return null; - } - try - { - int userId = Integer.parseInt(userStr); - if (!userService.userExists(userId)) - { - unsetUserCookie(response); - return null; - } - return userId; - } - catch (NumberFormatException e) - { - unsetUserCookie(response); - return null; - } - } + private Integer getCurrentUser(HttpServletRequest request, HttpServletResponse response) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return null; + } + String userStr = Arrays.stream(cookies) + .filter(c -> "user".equals(c.getName())) + .map(Cookie::getValue) + .findFirst() + .orElse(null); + if (userStr == null) { + return null; + } + try { + int userId = Integer.parseInt(userStr); + if (!userService.userExists(userId)) { + unsetUserCookie(response); + return null; + } + return userId; + } catch (NumberFormatException e) { + unsetUserCookie(response); + return null; + } + } - private void unsetUserCookie(HttpServletResponse response) - { - Cookie cookie = new Cookie("user", ""); - cookie.setMaxAge(0); - cookie.setPath("/"); - cookie.setHttpOnly(true); - response.addCookie(cookie); - } + private void unsetUserCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("user", ""); + cookie.setMaxAge(0); + cookie.setPath("/"); + cookie.setHttpOnly(true); + response.addCookie(cookie); + } - private String renderWithUser(Model model, int currentUser) - { - model.addAttribute("users", userService.getUsers()); - model.addAttribute("currentUser", currentUser); - model.addAttribute("allowances", allowanceService.getUserAllowances(currentUser)); - model.addAttribute("tasks", taskService.getTasks()); - model.addAttribute("history", allowanceService.getHistory(currentUser)); - return "index"; - } + private String renderWithUser(Model model, int currentUser) { + model.addAttribute("users", userService.getUsers()); + model.addAttribute("currentUser", currentUser); + model.addAttribute("allowances", allowanceService.getUserAllowances(currentUser)); + model.addAttribute("tasks", taskService.getTasks()); + model.addAttribute("history", allowanceService.getHistory(currentUser)); + return "index"; + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AddAllowanceAmountRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AddAllowanceAmountRequest.java index f1c467d..a8c393e 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AddAllowanceAmountRequest.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AddAllowanceAmountRequest.java @@ -1,5 +1,4 @@ package be.seeseepuff.allowanceplanner.dto; -public record AddAllowanceAmountRequest(double amount, String description) -{ +public record AddAllowanceAmountRequest(double amount, String description) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AllowanceDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AllowanceDto.java index fc7b2e7..9f4171d 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AllowanceDto.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/AllowanceDto.java @@ -3,6 +3,5 @@ package be.seeseepuff.allowanceplanner.dto; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.ALWAYS) -public record AllowanceDto(int id, String name, double target, double progress, double weight, String colour) -{ +public record AllowanceDto(int id, String name, double target, double progress, double weight, String colour) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/BulkUpdateAllowanceRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/BulkUpdateAllowanceRequest.java index 124a08c..7da0594 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/BulkUpdateAllowanceRequest.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/BulkUpdateAllowanceRequest.java @@ -1,5 +1,4 @@ package be.seeseepuff.allowanceplanner.dto; -public record BulkUpdateAllowanceRequest(int id, double weight) -{ +public record BulkUpdateAllowanceRequest(int id, double weight) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateAllowanceRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateAllowanceRequest.java index ac4e8aa..5af6e17 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateAllowanceRequest.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateAllowanceRequest.java @@ -1,5 +1,4 @@ package be.seeseepuff.allowanceplanner.dto; -public record CreateAllowanceRequest(String name, double target, double weight, String colour) -{ +public record CreateAllowanceRequest(String name, double target, double weight, String colour) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateTaskRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateTaskRequest.java index e430354..69e4146 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateTaskRequest.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/CreateTaskRequest.java @@ -1,11 +1,8 @@ package be.seeseepuff.allowanceplanner.dto; -import com.fasterxml.jackson.annotation.JsonProperty; - public record CreateTaskRequest( - String name, - Double reward, - Integer assigned, - String schedule) -{ + String name, + Double reward, + Integer assigned, + String schedule) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/ErrorResponse.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/ErrorResponse.java index c5763de..ed75413 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/ErrorResponse.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/ErrorResponse.java @@ -1,5 +1,4 @@ package be.seeseepuff.allowanceplanner.dto; -public record ErrorResponse(String error) -{ +public record ErrorResponse(String error) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/HistoryDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/HistoryDto.java index 7e404bd..63aa97f 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/HistoryDto.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/HistoryDto.java @@ -2,6 +2,5 @@ package be.seeseepuff.allowanceplanner.dto; import java.time.Instant; -public record HistoryDto(double allowance, Instant timestamp, String description) -{ +public record HistoryDto(double allowance, Instant timestamp, String description) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/IdResponse.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/IdResponse.java index 54b2bea..7bdbde3 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/IdResponse.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/IdResponse.java @@ -1,5 +1,4 @@ package be.seeseepuff.allowanceplanner.dto; -public record IdResponse(int id) -{ +public record IdResponse(int id) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MessageResponse.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MessageResponse.java index 2fef1f9..91dc296 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MessageResponse.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MessageResponse.java @@ -1,5 +1,4 @@ package be.seeseepuff.allowanceplanner.dto; -public record MessageResponse(String message) -{ +public record MessageResponse(String message) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MigrationDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MigrationDto.java index cda8f22..5a48819 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MigrationDto.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/MigrationDto.java @@ -3,19 +3,22 @@ package be.seeseepuff.allowanceplanner.dto; import java.util.List; public record MigrationDto( - List users, - List allowances, - List history, - List tasks -) -{ - public record MigrationUserDto(int id, String name, long balance, double weight) {} + List users, + List allowances, + List history, + List tasks +) { + public record MigrationUserDto(int id, String name, long balance, double weight) { + } - public record MigrationAllowanceDto(int id, int userId, String name, long target, long balance, double weight, - Integer colour) {} + public record MigrationAllowanceDto(int id, int userId, String name, long target, long balance, double weight, + Integer colour) { + } - public record MigrationHistoryDto(int id, int userId, long timestamp, long amount, String description) {} + public record MigrationHistoryDto(int id, int userId, long timestamp, long amount, String description) { + } - public record MigrationTaskDto(int id, String name, long reward, Integer assigned, String schedule, - Long completed, Long nextRun) {} + public record MigrationTaskDto(int id, String name, long reward, Integer assigned, String schedule, + Long completed, Long nextRun) { + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/PostHistoryRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/PostHistoryRequest.java index abff1a5..fffc9a7 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/PostHistoryRequest.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/PostHistoryRequest.java @@ -1,5 +1,4 @@ package be.seeseepuff.allowanceplanner.dto; -public record PostHistoryRequest(double allowance, String description) -{ +public record PostHistoryRequest(double allowance, String description) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TaskDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TaskDto.java index 168f10c..63d604d 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TaskDto.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TaskDto.java @@ -3,6 +3,5 @@ package be.seeseepuff.allowanceplanner.dto; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.ALWAYS) -public record TaskDto(int id, String name, double reward, Integer assigned, String schedule) -{ +public record TaskDto(int id, String name, double reward, Integer assigned, String schedule) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TransferRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TransferRequest.java index f631e40..ef4247b 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TransferRequest.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/TransferRequest.java @@ -1,5 +1,4 @@ package be.seeseepuff.allowanceplanner.dto; -public record TransferRequest(int from, int to, double amount) -{ +public record TransferRequest(int from, int to, double amount) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UpdateAllowanceRequest.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UpdateAllowanceRequest.java index 7b1cca8..14ab0a1 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UpdateAllowanceRequest.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UpdateAllowanceRequest.java @@ -1,5 +1,4 @@ package be.seeseepuff.allowanceplanner.dto; -public record UpdateAllowanceRequest(String name, double target, double weight, String colour) -{ +public record UpdateAllowanceRequest(String name, double target, double weight, String colour) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserDto.java index 18a0823..c498b22 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserDto.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserDto.java @@ -1,5 +1,4 @@ package be.seeseepuff.allowanceplanner.dto; -public record UserDto(int id, String name) -{ +public record UserDto(int id, String name) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserWithAllowanceDto.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserWithAllowanceDto.java index 553ddc0..32e3ad8 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserWithAllowanceDto.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/dto/UserWithAllowanceDto.java @@ -1,5 +1,4 @@ package be.seeseepuff.allowanceplanner.dto; -public record UserWithAllowanceDto(int id, String name, double allowance) -{ +public record UserWithAllowanceDto(int id, String name, double allowance) { } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java index f562b25..68e02cb 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java @@ -4,96 +4,81 @@ import jakarta.persistence.*; @Entity @Table(name = "allowances") -public class Allowance -{ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; +public class Allowance { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; - @Column(name = "user_id", nullable = false) - private int userId; + @Column(name = "user_id", nullable = false) + private int userId; - @Column(nullable = false) - private String name; + @Column(nullable = false) + private String name; - @Column(nullable = false) - private long target; + @Column(nullable = false) + private long target; - @Column(nullable = false) - private long balance = 0; + @Column(nullable = false) + private long balance = 0; - @Column(nullable = false) - private double weight; + @Column(nullable = false) + private double weight; - private Integer colour; + private Integer colour; - public int getId() - { - return id; - } + public int getId() { + return id; + } - public void setId(int id) - { - this.id = id; - } + public void setId(int id) { + this.id = id; + } - public int getUserId() - { - return userId; - } + public int getUserId() { + return userId; + } - public void setUserId(int userId) - { - this.userId = userId; - } + public void setUserId(int userId) { + this.userId = userId; + } - public String getName() - { - return name; - } + public String getName() { + return name; + } - public void setName(String name) - { - this.name = name; - } + public void setName(String name) { + this.name = name; + } - public long getTarget() - { - return target; - } + public long getTarget() { + return target; + } - public void setTarget(long target) - { - this.target = target; - } + public void setTarget(long target) { + this.target = target; + } - public long getBalance() - { - return balance; - } + public long getBalance() { + return balance; + } - public void setBalance(long balance) - { - this.balance = balance; - } + public void setBalance(long balance) { + this.balance = balance; + } - public double getWeight() - { - return weight; - } + public double getWeight() { + return weight; + } - public void setWeight(double weight) - { - this.weight = weight; - } + public void setWeight(double weight) { + this.weight = weight; + } - public Integer getColour() - { - return colour; - } + public Integer getColour() { + return colour; + } - public void setColour(Integer colour) - { - this.colour = colour; - } + public void setColour(Integer colour) { + this.colour = colour; + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java index 520422b..6f7340e 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java @@ -4,70 +4,59 @@ import jakarta.persistence.*; @Entity @Table(name = "history") -public class History -{ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; +public class History { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; - @Column(name = "user_id", nullable = false) - private int userId; + @Column(name = "user_id", nullable = false) + private int userId; - @Column(nullable = false) - private long timestamp; + @Column(nullable = false) + private long timestamp; - @Column(nullable = false) - private long amount; + @Column(nullable = false) + private long amount; - private String description; + private String description; - public int getId() - { - return id; - } + public int getId() { + return id; + } - public void setId(int id) - { - this.id = id; - } + public void setId(int id) { + this.id = id; + } - public int getUserId() - { - return userId; - } + public int getUserId() { + return userId; + } - public void setUserId(int userId) - { - this.userId = userId; - } + public void setUserId(int userId) { + this.userId = userId; + } - public long getTimestamp() - { - return timestamp; - } + public long getTimestamp() { + return timestamp; + } - public void setTimestamp(long timestamp) - { - this.timestamp = timestamp; - } + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } - public long getAmount() - { - return amount; - } + public long getAmount() { + return amount; + } - public void setAmount(long amount) - { - this.amount = amount; - } + public void setAmount(long amount) { + this.amount = amount; + } - public String getDescription() - { - return description; - } + public String getDescription() { + return description; + } - public void setDescription(String description) - { - this.description = description; - } + public void setDescription(String description) { + this.description = description; + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java index 474c98e..da662e1 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java @@ -4,94 +4,79 @@ import jakarta.persistence.*; @Entity @Table(name = "tasks") -public class Task -{ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; +public class Task { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; - @Column(nullable = false) - private String name; + @Column(nullable = false) + private String name; - @Column(nullable = false) - private long reward; + @Column(nullable = false) + private long reward; - private Integer assigned; + private Integer assigned; - private String schedule; + private String schedule; - private Long completed; + private Long completed; - @Column(name = "next_run") - private Long nextRun; + @Column(name = "next_run") + private Long nextRun; - public int getId() - { - return id; - } + public int getId() { + return id; + } - public void setId(int id) - { - this.id = id; - } + public void setId(int id) { + this.id = id; + } - public String getName() - { - return name; - } + public String getName() { + return name; + } - public void setName(String name) - { - this.name = name; - } + public void setName(String name) { + this.name = name; + } - public long getReward() - { - return reward; - } + public long getReward() { + return reward; + } - public void setReward(long reward) - { - this.reward = reward; - } + public void setReward(long reward) { + this.reward = reward; + } - public Integer getAssigned() - { - return assigned; - } + public Integer getAssigned() { + return assigned; + } - public void setAssigned(Integer assigned) - { - this.assigned = assigned; - } + public void setAssigned(Integer assigned) { + this.assigned = assigned; + } - public String getSchedule() - { - return schedule; - } + public String getSchedule() { + return schedule; + } - public void setSchedule(String schedule) - { - this.schedule = schedule; - } + public void setSchedule(String schedule) { + this.schedule = schedule; + } - public Long getCompleted() - { - return completed; - } + public Long getCompleted() { + return completed; + } - public void setCompleted(Long completed) - { - this.completed = completed; - } + public void setCompleted(Long completed) { + this.completed = completed; + } - public Long getNextRun() - { - return nextRun; - } + public Long getNextRun() { + return nextRun; + } - public void setNextRun(Long nextRun) - { - this.nextRun = nextRun; - } + public void setNextRun(Long nextRun) { + this.nextRun = nextRun; + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java index 5b1b501..8a74483 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java @@ -4,58 +4,49 @@ import jakarta.persistence.*; @Entity @Table(name = "users") -public class User -{ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; - @Column(nullable = false) - private String name; + @Column(nullable = false) + private String name; - @Column(nullable = false) - private double weight = 10.0; + @Column(nullable = false) + private double weight = 10.0; - @Column(nullable = false) - private long balance = 0; + @Column(nullable = false) + private long balance = 0; - public int getId() - { - return id; - } + public int getId() { + return id; + } - public void setId(int id) - { - this.id = id; - } + public void setId(int id) { + this.id = id; + } - public String getName() - { - return name; - } + public String getName() { + return name; + } - public void setName(String name) - { - this.name = name; - } + public void setName(String name) { + this.name = name; + } - public double getWeight() - { - return weight; - } + public double getWeight() { + return weight; + } - public void setWeight(double weight) - { - this.weight = weight; - } + public void setWeight(double weight) { + this.weight = weight; + } - public long getBalance() - { - return balance; - } + public long getBalance() { + return balance; + } - public void setBalance(long balance) - { - this.balance = balance; - } + public void setBalance(long balance) { + this.balance = balance; + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/AllowanceRepository.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/AllowanceRepository.java index ba79bfb..33ab473 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/AllowanceRepository.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/AllowanceRepository.java @@ -9,19 +9,18 @@ import java.util.List; import java.util.Optional; @Repository -public interface AllowanceRepository extends JpaRepository -{ - List findByUserIdOrderByIdAsc(int userId); +public interface AllowanceRepository extends JpaRepository { + List findByUserIdOrderByIdAsc(int userId); - Optional findByIdAndUserId(int id, int userId); + Optional findByIdAndUserId(int id, int userId); - int countByIdAndUserId(int id, int userId); + int countByIdAndUserId(int id, int userId); - void deleteByIdAndUserId(int id, int userId); + void deleteByIdAndUserId(int id, int userId); - @Query("SELECT a FROM Allowance a WHERE a.userId = :userId AND a.weight > 0 ORDER BY (a.target - a.balance) ASC") - List findByUserIdWithPositiveWeightOrderByRemainingAsc(int userId); + @Query("SELECT a FROM Allowance a WHERE a.userId = :userId AND a.weight > 0 ORDER BY (a.target - a.balance) ASC") + List findByUserIdWithPositiveWeightOrderByRemainingAsc(int userId); - @Query("SELECT COALESCE(SUM(a.weight), 0) FROM Allowance a WHERE a.userId = :userId AND a.weight > 0") - double sumPositiveWeights(int userId); + @Query("SELECT COALESCE(SUM(a.weight), 0) FROM Allowance a WHERE a.userId = :userId AND a.weight > 0") + double sumPositiveWeights(int userId); } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/HistoryRepository.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/HistoryRepository.java index 67030fe..0d3a849 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/HistoryRepository.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/HistoryRepository.java @@ -7,7 +7,6 @@ import org.springframework.stereotype.Repository; import java.util.List; @Repository -public interface HistoryRepository extends JpaRepository -{ - List findByUserIdOrderByIdDesc(int userId); +public interface HistoryRepository extends JpaRepository { + List findByUserIdOrderByIdDesc(int userId); } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/TaskRepository.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/TaskRepository.java index 7e6fac0..9c6154a 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/TaskRepository.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/TaskRepository.java @@ -8,9 +8,8 @@ import java.util.List; import java.util.Optional; @Repository -public interface TaskRepository extends JpaRepository -{ - List findByCompletedIsNull(); +public interface TaskRepository extends JpaRepository { + List findByCompletedIsNull(); - Optional findByIdAndCompletedIsNull(int id); + Optional findByIdAndCompletedIsNull(int id); } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/UserRepository.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/UserRepository.java index edf7723..45c1b9e 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/UserRepository.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/repository/UserRepository.java @@ -6,8 +6,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository -public interface UserRepository extends JpaRepository -{ - @Query("SELECT COALESCE(SUM(h.amount), 0) FROM History h WHERE h.userId = :userId") - long sumHistoryAmount(int userId); +public interface UserRepository extends JpaRepository { + @Query("SELECT COALESCE(SUM(h.amount), 0) FROM History h WHERE h.userId = :userId") + long sumHistoryAmount(int userId); } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/AllowanceService.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/AllowanceService.java index 0b3d6a7..929ff8a 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/AllowanceService.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/AllowanceService.java @@ -17,282 +17,242 @@ import java.util.List; import java.util.Optional; @Service -public class AllowanceService -{ - private final AllowanceRepository allowanceRepository; - private final UserRepository userRepository; - private final HistoryRepository historyRepository; +public class AllowanceService { + private final AllowanceRepository allowanceRepository; + private final UserRepository userRepository; + private final HistoryRepository historyRepository; - public AllowanceService(AllowanceRepository allowanceRepository, - UserRepository userRepository, - HistoryRepository historyRepository) - { - this.allowanceRepository = allowanceRepository; - this.userRepository = userRepository; - this.historyRepository = historyRepository; - } + public AllowanceService(AllowanceRepository allowanceRepository, + UserRepository userRepository, + HistoryRepository historyRepository) { + this.allowanceRepository = allowanceRepository; + this.userRepository = userRepository; + this.historyRepository = historyRepository; + } - public List getUserAllowances(int userId) - { - User user = userRepository.findById(userId).orElseThrow(); - List result = new ArrayList<>(); + public List getUserAllowances(int userId) { + User user = userRepository.findById(userId).orElseThrow(); + List result = new ArrayList<>(); - // Add the "rest" allowance (id=0) - result.add(new AllowanceDto(0, "", 0, user.getBalance() / 100.0, user.getWeight(), "")); + // Add the "rest" allowance (id=0) + result.add(new AllowanceDto(0, "", 0, user.getBalance() / 100.0, user.getWeight(), "")); - // Add named allowances - for (Allowance a : allowanceRepository.findByUserIdOrderByIdAsc(userId)) - { - result.add(toDto(a)); - } - return result; - } + // Add named allowances + for (Allowance a : allowanceRepository.findByUserIdOrderByIdAsc(userId)) { + result.add(toDto(a)); + } + return result; + } - public Optional getUserAllowanceById(int userId, int allowanceId) - { - if (allowanceId == 0) - { - return userRepository.findById(userId) - .map(u -> new AllowanceDto(0, "", 0, u.getBalance() / 100.0, u.getWeight(), "")); - } - return allowanceRepository.findByIdAndUserId(allowanceId, userId) - .map(this::toDto); - } + public Optional getUserAllowanceById(int userId, int allowanceId) { + if (allowanceId == 0) { + return userRepository.findById(userId) + .map(u -> new AllowanceDto(0, "", 0, u.getBalance() / 100.0, u.getWeight(), "")); + } + return allowanceRepository.findByIdAndUserId(allowanceId, userId) + .map(this::toDto); + } - @Transactional - public int createAllowance(int userId, CreateAllowanceRequest request) - { - int colour = ColourUtil.convertStringToColour(request.colour()); - Allowance allowance = new Allowance(); - allowance.setUserId(userId); - allowance.setName(request.name()); - allowance.setTarget(Math.round(request.target() * 100.0)); - allowance.setWeight(request.weight()); - allowance.setColour(colour); - allowance = allowanceRepository.save(allowance); - return allowance.getId(); - } + @Transactional + public int createAllowance(int userId, CreateAllowanceRequest request) { + int colour = ColourUtil.convertStringToColour(request.colour()); + Allowance allowance = new Allowance(); + allowance.setUserId(userId); + allowance.setName(request.name()); + allowance.setTarget(Math.round(request.target() * 100.0)); + allowance.setWeight(request.weight()); + allowance.setColour(colour); + allowance = allowanceRepository.save(allowance); + return allowance.getId(); + } - @Transactional - public boolean deleteAllowance(int userId, int allowanceId) - { - int count = allowanceRepository.countByIdAndUserId(allowanceId, userId); - if (count == 0) - { - return false; - } - allowanceRepository.deleteByIdAndUserId(allowanceId, userId); - return true; - } + @Transactional + public boolean deleteAllowance(int userId, int allowanceId) { + int count = allowanceRepository.countByIdAndUserId(allowanceId, userId); + if (count == 0) { + return false; + } + allowanceRepository.deleteByIdAndUserId(allowanceId, userId); + return true; + } - @Transactional - public boolean updateAllowance(int userId, int allowanceId, UpdateAllowanceRequest request) - { - if (allowanceId == 0) - { - User user = userRepository.findById(userId).orElseThrow(); - user.setWeight(request.weight()); - userRepository.save(user); - return true; - } + @Transactional + public boolean updateAllowance(int userId, int allowanceId, UpdateAllowanceRequest request) { + if (allowanceId == 0) { + User user = userRepository.findById(userId).orElseThrow(); + user.setWeight(request.weight()); + userRepository.save(user); + return true; + } - Optional opt = allowanceRepository.findByIdAndUserId(allowanceId, userId); - if (opt.isEmpty()) - { - return false; - } + Optional opt = allowanceRepository.findByIdAndUserId(allowanceId, userId); + if (opt.isEmpty()) { + return false; + } - int colour = ColourUtil.convertStringToColour(request.colour()); - Allowance allowance = opt.get(); - allowance.setName(request.name()); - allowance.setTarget(Math.round(request.target() * 100.0)); - allowance.setWeight(request.weight()); - allowance.setColour(colour); - allowanceRepository.save(allowance); - return true; - } + int colour = ColourUtil.convertStringToColour(request.colour()); + Allowance allowance = opt.get(); + allowance.setName(request.name()); + allowance.setTarget(Math.round(request.target() * 100.0)); + allowance.setWeight(request.weight()); + allowance.setColour(colour); + allowanceRepository.save(allowance); + return true; + } - @Transactional - public void bulkUpdateAllowance(int userId, List requests) - { - for (BulkUpdateAllowanceRequest req : requests) - { - if (req.id() == 0) - { - User user = userRepository.findById(userId).orElseThrow(); - user.setWeight(req.weight()); - userRepository.save(user); - } - else - { - allowanceRepository.findByIdAndUserId(req.id(), userId).ifPresent(a -> - { - a.setWeight(req.weight()); - allowanceRepository.save(a); - }); - } - } - } + @Transactional + public void bulkUpdateAllowance(int userId, List requests) { + for (BulkUpdateAllowanceRequest req : requests) { + if (req.id() == 0) { + User user = userRepository.findById(userId).orElseThrow(); + user.setWeight(req.weight()); + userRepository.save(user); + } else { + allowanceRepository.findByIdAndUserId(req.id(), userId).ifPresent(a -> + { + a.setWeight(req.weight()); + allowanceRepository.save(a); + }); + } + } + } - @Transactional - public boolean completeAllowance(int userId, int allowanceId) - { - Optional opt = allowanceRepository.findByIdAndUserId(allowanceId, userId); - if (opt.isEmpty()) - { - return false; - } + @Transactional + public boolean completeAllowance(int userId, int allowanceId) { + Optional opt = allowanceRepository.findByIdAndUserId(allowanceId, userId); + if (opt.isEmpty()) { + return false; + } - Allowance allowance = opt.get(); - long cost = allowance.getBalance(); - String allowanceName = allowance.getName(); + Allowance allowance = opt.get(); + long cost = allowance.getBalance(); + String allowanceName = allowance.getName(); - // Delete the allowance - allowanceRepository.delete(allowance); + // Delete the allowance + allowanceRepository.delete(allowance); - // Add a history entry - History history = new History(); - history.setUserId(userId); - history.setTimestamp(Instant.now().getEpochSecond()); - history.setAmount(-cost); - history.setDescription("Allowance completed: " + allowanceName); - historyRepository.save(history); + // Add a history entry + History history = new History(); + history.setUserId(userId); + history.setTimestamp(Instant.now().getEpochSecond()); + history.setAmount(-cost); + history.setDescription("Allowance completed: " + allowanceName); + historyRepository.save(history); - return true; - } + return true; + } - @Transactional - public boolean addAllowanceAmount(int userId, int allowanceId, AddAllowanceAmountRequest request) - { - long remainingAmount = Math.round(request.amount() * 100); + @Transactional + public boolean addAllowanceAmount(int userId, int allowanceId, AddAllowanceAmountRequest request) { + long remainingAmount = Math.round(request.amount() * 100); - // Insert history entry - History history = new History(); - history.setUserId(userId); - history.setTimestamp(Instant.now().getEpochSecond()); - history.setAmount(remainingAmount); - history.setDescription(request.description()); - historyRepository.save(history); + // Insert history entry + History history = new History(); + history.setUserId(userId); + history.setTimestamp(Instant.now().getEpochSecond()); + history.setAmount(remainingAmount); + history.setDescription(request.description()); + historyRepository.save(history); - if (allowanceId == 0) - { - if (remainingAmount < 0) - { - User user = userRepository.findById(userId).orElseThrow(); - if (remainingAmount > user.getBalance()) - { - throw new IllegalArgumentException("cannot remove more than the current balance: " + user.getBalance()); - } - } - User user = userRepository.findById(userId).orElseThrow(); - user.setBalance(user.getBalance() + remainingAmount); - userRepository.save(user); - } - else if (remainingAmount < 0) - { - Allowance allowance = allowanceRepository.findByIdAndUserId(allowanceId, userId).orElse(null); - if (allowance == null) - { - return false; - } - if (remainingAmount > allowance.getBalance()) - { - throw new IllegalArgumentException("cannot remove more than the current allowance balance: " + allowance.getBalance()); - } - allowance.setBalance(allowance.getBalance() + remainingAmount); - allowanceRepository.save(allowance); - } - else - { - Allowance allowance = allowanceRepository.findByIdAndUserId(allowanceId, userId).orElse(null); - if (allowance == null) - { - return false; - } + if (allowanceId == 0) { + if (remainingAmount < 0) { + User user = userRepository.findById(userId).orElseThrow(); + if (remainingAmount > user.getBalance()) { + throw new IllegalArgumentException("cannot remove more than the current balance: " + user.getBalance()); + } + } + User user = userRepository.findById(userId).orElseThrow(); + user.setBalance(user.getBalance() + remainingAmount); + userRepository.save(user); + } else if (remainingAmount < 0) { + Allowance allowance = allowanceRepository.findByIdAndUserId(allowanceId, userId).orElse(null); + if (allowance == null) { + return false; + } + if (remainingAmount > allowance.getBalance()) { + throw new IllegalArgumentException("cannot remove more than the current allowance balance: " + allowance.getBalance()); + } + allowance.setBalance(allowance.getBalance() + remainingAmount); + allowanceRepository.save(allowance); + } else { + Allowance allowance = allowanceRepository.findByIdAndUserId(allowanceId, userId).orElse(null); + if (allowance == null) { + return false; + } - long toAdd = remainingAmount; - if (allowance.getBalance() + toAdd > allowance.getTarget()) - { - toAdd = allowance.getTarget() - allowance.getBalance(); - } - remainingAmount -= toAdd; + long toAdd = remainingAmount; + if (allowance.getBalance() + toAdd > allowance.getTarget()) { + toAdd = allowance.getTarget() - allowance.getBalance(); + } + remainingAmount -= toAdd; - if (toAdd > 0) - { - allowance.setBalance(allowance.getBalance() + toAdd); - allowanceRepository.save(allowance); - } + if (toAdd > 0) { + allowance.setBalance(allowance.getBalance() + toAdd); + allowanceRepository.save(allowance); + } - if (remainingAmount > 0) - { - addDistributedReward(userId, (int) remainingAmount); - } - } - return true; - } + if (remainingAmount > 0) { + addDistributedReward(userId, (int) remainingAmount); + } + } + return true; + } - public void addDistributedReward(int userId, int reward) - { - User user = userRepository.findById(userId).orElseThrow(); - double userWeight = user.getWeight(); + public void addDistributedReward(int userId, int reward) { + User user = userRepository.findById(userId).orElseThrow(); + double userWeight = user.getWeight(); - double sumOfWeights = allowanceRepository.sumPositiveWeights(userId) + userWeight; + double sumOfWeights = allowanceRepository.sumPositiveWeights(userId) + userWeight; - int remainingReward = reward; + int remainingReward = reward; - if (sumOfWeights > 0) - { - List allowances = allowanceRepository.findByUserIdWithPositiveWeightOrderByRemainingAsc(userId); - for (Allowance allowance : allowances) - { - int amount = (int) ((allowance.getWeight() / sumOfWeights) * remainingReward); - if (allowance.getBalance() + amount > allowance.getTarget()) - { - amount = (int) (allowance.getTarget() - allowance.getBalance()); - } - sumOfWeights -= allowance.getWeight(); - allowance.setBalance(allowance.getBalance() + amount); - allowanceRepository.save(allowance); - remainingReward -= amount; - } - } + if (sumOfWeights > 0) { + List allowances = allowanceRepository.findByUserIdWithPositiveWeightOrderByRemainingAsc(userId); + for (Allowance allowance : allowances) { + int amount = (int) ((allowance.getWeight() / sumOfWeights) * remainingReward); + if (allowance.getBalance() + amount > allowance.getTarget()) { + amount = (int) (allowance.getTarget() - allowance.getBalance()); + } + sumOfWeights -= allowance.getWeight(); + allowance.setBalance(allowance.getBalance() + amount); + allowanceRepository.save(allowance); + remainingReward -= amount; + } + } - // Add remaining to user's balance - user = userRepository.findById(userId).orElseThrow(); - user.setBalance(user.getBalance() + remainingReward); - userRepository.save(user); - } + // Add remaining to user's balance + user = userRepository.findById(userId).orElseThrow(); + user.setBalance(user.getBalance() + remainingReward); + userRepository.save(user); + } - public List getHistory(int userId) - { - return historyRepository.findByUserIdOrderByIdDesc(userId).stream() - .map(h -> new HistoryDto( - h.getAmount() / 100.0, - Instant.ofEpochSecond(h.getTimestamp()), - h.getDescription())) - .toList(); - } + public List getHistory(int userId) { + return historyRepository.findByUserIdOrderByIdDesc(userId).stream() + .map(h -> new HistoryDto( + h.getAmount() / 100.0, + Instant.ofEpochSecond(h.getTimestamp()), + h.getDescription())) + .toList(); + } - @Transactional - public void addHistory(int userId, PostHistoryRequest request) - { - long amount = Math.round(request.allowance() * 100.0); - History history = new History(); - history.setUserId(userId); - history.setTimestamp(Instant.now().getEpochSecond()); - history.setAmount(amount); - history.setDescription(request.description()); - historyRepository.save(history); - } + @Transactional + public void addHistory(int userId, PostHistoryRequest request) { + long amount = Math.round(request.allowance() * 100.0); + History history = new History(); + history.setUserId(userId); + history.setTimestamp(Instant.now().getEpochSecond()); + history.setAmount(amount); + history.setDescription(request.description()); + historyRepository.save(history); + } - private AllowanceDto toDto(Allowance a) - { - return new AllowanceDto( - a.getId(), - a.getName(), - a.getTarget() / 100.0, - a.getBalance() / 100.0, - a.getWeight(), - ColourUtil.convertColourToString(a.getColour())); - } + private AllowanceDto toDto(Allowance a) { + return new AllowanceDto( + a.getId(), + a.getName(), + a.getTarget() / 100.0, + a.getBalance() / 100.0, + a.getWeight(), + ColourUtil.convertColourToString(a.getColour())); + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/MigrationService.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/MigrationService.java index e717ffb..80f2261 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/MigrationService.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/MigrationService.java @@ -10,95 +10,88 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service -public class MigrationService -{ - private final UserRepository userRepository; - private final AllowanceRepository allowanceRepository; - private final HistoryRepository historyRepository; - private final TaskRepository taskRepository; - private final EntityManager entityManager; +public class MigrationService { + private final UserRepository userRepository; + private final AllowanceRepository allowanceRepository; + private final HistoryRepository historyRepository; + private final TaskRepository taskRepository; + private final EntityManager entityManager; - public MigrationService(UserRepository userRepository, - AllowanceRepository allowanceRepository, - HistoryRepository historyRepository, - TaskRepository taskRepository, - EntityManager entityManager) - { - this.userRepository = userRepository; - this.allowanceRepository = allowanceRepository; - this.historyRepository = historyRepository; - this.taskRepository = taskRepository; - this.entityManager = entityManager; - } + public MigrationService(UserRepository userRepository, + AllowanceRepository allowanceRepository, + HistoryRepository historyRepository, + TaskRepository taskRepository, + EntityManager entityManager) { + this.userRepository = userRepository; + this.allowanceRepository = allowanceRepository; + this.historyRepository = historyRepository; + this.taskRepository = taskRepository; + this.entityManager = entityManager; + } - @Transactional - public void importData(MigrationDto data) - { - // Delete in dependency order - taskRepository.deleteAll(); - historyRepository.deleteAll(); - allowanceRepository.deleteAll(); - userRepository.deleteAll(); + @Transactional + public void importData(MigrationDto data) { + // Delete in dependency order + taskRepository.deleteAll(); + historyRepository.deleteAll(); + allowanceRepository.deleteAll(); + userRepository.deleteAll(); - // Insert users with original IDs using native SQL to bypass auto-increment - for (MigrationDto.MigrationUserDto u : data.users()) - { - entityManager.createNativeQuery( - "INSERT INTO users (id, name, balance, weight) VALUES (:id, :name, :balance, :weight)") - .setParameter("id", u.id()) - .setParameter("name", u.name()) - .setParameter("balance", u.balance()) - .setParameter("weight", u.weight()) - .executeUpdate(); - } + // Insert users with original IDs using native SQL to bypass auto-increment + for (MigrationDto.MigrationUserDto u : data.users()) { + entityManager.createNativeQuery( + "INSERT INTO users (id, name, balance, weight) VALUES (:id, :name, :balance, :weight)") + .setParameter("id", u.id()) + .setParameter("name", u.name()) + .setParameter("balance", u.balance()) + .setParameter("weight", u.weight()) + .executeUpdate(); + } - // Insert allowances with original IDs - for (MigrationDto.MigrationAllowanceDto a : data.allowances()) - { - entityManager.createNativeQuery( - "INSERT INTO allowances (id, user_id, name, target, balance, weight, colour) VALUES (:id, :userId, :name, :target, :balance, :weight, :colour)") - .setParameter("id", a.id()) - .setParameter("userId", a.userId()) - .setParameter("name", a.name()) - .setParameter("target", a.target()) - .setParameter("balance", a.balance()) - .setParameter("weight", a.weight()) - .setParameter("colour", a.colour()) - .executeUpdate(); - } + // Insert allowances with original IDs + for (MigrationDto.MigrationAllowanceDto a : data.allowances()) { + entityManager.createNativeQuery( + "INSERT INTO allowances (id, user_id, name, target, balance, weight, colour) VALUES (:id, :userId, :name, :target, :balance, :weight, :colour)") + .setParameter("id", a.id()) + .setParameter("userId", a.userId()) + .setParameter("name", a.name()) + .setParameter("target", a.target()) + .setParameter("balance", a.balance()) + .setParameter("weight", a.weight()) + .setParameter("colour", a.colour()) + .executeUpdate(); + } - // Insert history with original IDs - for (MigrationDto.MigrationHistoryDto h : data.history()) - { - entityManager.createNativeQuery( - "INSERT INTO history (id, user_id, timestamp, amount, description) VALUES (:id, :userId, :timestamp, :amount, :description)") - .setParameter("id", h.id()) - .setParameter("userId", h.userId()) - .setParameter("timestamp", h.timestamp()) - .setParameter("amount", h.amount()) - .setParameter("description", h.description()) - .executeUpdate(); - } + // Insert history with original IDs + for (MigrationDto.MigrationHistoryDto h : data.history()) { + entityManager.createNativeQuery( + "INSERT INTO history (id, user_id, timestamp, amount, description) VALUES (:id, :userId, :timestamp, :amount, :description)") + .setParameter("id", h.id()) + .setParameter("userId", h.userId()) + .setParameter("timestamp", h.timestamp()) + .setParameter("amount", h.amount()) + .setParameter("description", h.description()) + .executeUpdate(); + } - // Insert tasks with original IDs - for (MigrationDto.MigrationTaskDto t : data.tasks()) - { - entityManager.createNativeQuery( - "INSERT INTO tasks (id, name, reward, assigned, schedule, completed, next_run) VALUES (:id, :name, :reward, :assigned, :schedule, :completed, :nextRun)") - .setParameter("id", t.id()) - .setParameter("name", t.name()) - .setParameter("reward", t.reward()) - .setParameter("assigned", t.assigned()) - .setParameter("schedule", t.schedule()) - .setParameter("completed", t.completed()) - .setParameter("nextRun", t.nextRun()) - .executeUpdate(); - } + // Insert tasks with original IDs + for (MigrationDto.MigrationTaskDto t : data.tasks()) { + entityManager.createNativeQuery( + "INSERT INTO tasks (id, name, reward, assigned, schedule, completed, next_run) VALUES (:id, :name, :reward, :assigned, :schedule, :completed, :nextRun)") + .setParameter("id", t.id()) + .setParameter("name", t.name()) + .setParameter("reward", t.reward()) + .setParameter("assigned", t.assigned()) + .setParameter("schedule", t.schedule()) + .setParameter("completed", t.completed()) + .setParameter("nextRun", t.nextRun()) + .executeUpdate(); + } - // Reset sequences so new inserts don't collide with migrated IDs - entityManager.createNativeQuery("SELECT setval('users_id_seq', COALESCE((SELECT MAX(id) FROM users), 0))").getSingleResult(); - entityManager.createNativeQuery("SELECT setval('allowances_id_seq', COALESCE((SELECT MAX(id) FROM allowances), 0))").getSingleResult(); - entityManager.createNativeQuery("SELECT setval('history_id_seq', COALESCE((SELECT MAX(id) FROM history), 0))").getSingleResult(); - entityManager.createNativeQuery("SELECT setval('tasks_id_seq', COALESCE((SELECT MAX(id) FROM tasks), 0))").getSingleResult(); - } + // Reset sequences so new inserts don't collide with migrated IDs + entityManager.createNativeQuery("SELECT setval('users_id_seq', COALESCE((SELECT MAX(id) FROM users), 0))").getSingleResult(); + entityManager.createNativeQuery("SELECT setval('allowances_id_seq', COALESCE((SELECT MAX(id) FROM allowances), 0))").getSingleResult(); + entityManager.createNativeQuery("SELECT setval('history_id_seq', COALESCE((SELECT MAX(id) FROM history), 0))").getSingleResult(); + entityManager.createNativeQuery("SELECT setval('tasks_id_seq', COALESCE((SELECT MAX(id) FROM tasks), 0))").getSingleResult(); + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TaskService.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TaskService.java index 47dd254..9841278 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TaskService.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TaskService.java @@ -1,6 +1,7 @@ package be.seeseepuff.allowanceplanner.service; -import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.dto.CreateTaskRequest; +import be.seeseepuff.allowanceplanner.dto.TaskDto; import be.seeseepuff.allowanceplanner.entity.History; import be.seeseepuff.allowanceplanner.entity.Task; import be.seeseepuff.allowanceplanner.entity.User; @@ -15,118 +16,105 @@ import java.util.List; import java.util.Optional; @Service -public class TaskService -{ - private final TaskRepository taskRepository; - private final UserRepository userRepository; - private final HistoryRepository historyRepository; - private final AllowanceService allowanceService; +public class TaskService { + private final TaskRepository taskRepository; + private final UserRepository userRepository; + private final HistoryRepository historyRepository; + private final AllowanceService allowanceService; - public TaskService(TaskRepository taskRepository, - UserRepository userRepository, - HistoryRepository historyRepository, - AllowanceService allowanceService) - { - this.taskRepository = taskRepository; - this.userRepository = userRepository; - this.historyRepository = historyRepository; - this.allowanceService = allowanceService; - } + public TaskService(TaskRepository taskRepository, + UserRepository userRepository, + HistoryRepository historyRepository, + AllowanceService allowanceService) { + this.taskRepository = taskRepository; + this.userRepository = userRepository; + this.historyRepository = historyRepository; + this.allowanceService = allowanceService; + } - @Transactional - public int createTask(CreateTaskRequest request) - { - Task task = new Task(); - task.setName(request.name()); - task.setReward(Math.round((request.reward() != null ? request.reward() : 0.0) * 100.0)); - task.setAssigned(request.assigned()); - task = taskRepository.save(task); - return task.getId(); - } + @Transactional + public int createTask(CreateTaskRequest request) { + Task task = new Task(); + task.setName(request.name()); + task.setReward(Math.round((request.reward() != null ? request.reward() : 0.0) * 100.0)); + task.setAssigned(request.assigned()); + task = taskRepository.save(task); + return task.getId(); + } - public List getTasks() - { - return taskRepository.findByCompletedIsNull().stream() - .map(this::toDto) - .toList(); - } + public List getTasks() { + return taskRepository.findByCompletedIsNull().stream() + .map(this::toDto) + .toList(); + } - public Optional getTask(int taskId) - { - return taskRepository.findByIdAndCompletedIsNull(taskId) - .map(this::toDto); - } + public Optional getTask(int taskId) { + return taskRepository.findByIdAndCompletedIsNull(taskId) + .map(this::toDto); + } - @Transactional - public boolean updateTask(int taskId, CreateTaskRequest request) - { - Optional opt = taskRepository.findByIdAndCompletedIsNull(taskId); - if (opt.isEmpty()) - { - return false; - } - Task task = opt.get(); - task.setName(request.name()); - task.setReward(Math.round((request.reward() != null ? request.reward() : 0.0) * 100.0)); - task.setAssigned(request.assigned()); - taskRepository.save(task); - return true; - } + @Transactional + public boolean updateTask(int taskId, CreateTaskRequest request) { + Optional opt = taskRepository.findByIdAndCompletedIsNull(taskId); + if (opt.isEmpty()) { + return false; + } + Task task = opt.get(); + task.setName(request.name()); + task.setReward(Math.round((request.reward() != null ? request.reward() : 0.0) * 100.0)); + task.setAssigned(request.assigned()); + taskRepository.save(task); + return true; + } - public boolean hasTask(int taskId) - { - return taskRepository.existsById(taskId); - } + public boolean hasTask(int taskId) { + return taskRepository.existsById(taskId); + } - @Transactional - public void deleteTask(int taskId) - { - taskRepository.deleteById(taskId); - } + @Transactional + public void deleteTask(int taskId) { + taskRepository.deleteById(taskId); + } - @Transactional - public boolean completeTask(int taskId) - { - Optional opt = taskRepository.findById(taskId); - if (opt.isEmpty()) - { - return false; - } + @Transactional + public boolean completeTask(int taskId) { + Optional opt = taskRepository.findById(taskId); + if (opt.isEmpty()) { + return false; + } - Task task = opt.get(); - long reward = task.getReward(); - String rewardName = task.getName(); + Task task = opt.get(); + long reward = task.getReward(); + String rewardName = task.getName(); - // Give reward to all users - List users = userRepository.findAll(); - for (User user : users) - { - // Add history entry - History history = new History(); - history.setUserId(user.getId()); - history.setTimestamp(Instant.now().getEpochSecond()); - history.setAmount(reward); - history.setDescription("Task completed: " + rewardName); - historyRepository.save(history); + // Give reward to all users + List users = userRepository.findAll(); + for (User user : users) { + // Add history entry + History history = new History(); + history.setUserId(user.getId()); + history.setTimestamp(Instant.now().getEpochSecond()); + history.setAmount(reward); + history.setDescription("Task completed: " + rewardName); + historyRepository.save(history); - // Distribute reward - allowanceService.addDistributedReward(user.getId(), (int) reward); - } + // Distribute reward + allowanceService.addDistributedReward(user.getId(), (int) reward); + } - // Mark task as completed - task.setCompleted(Instant.now().getEpochSecond()); - taskRepository.save(task); + // Mark task as completed + task.setCompleted(Instant.now().getEpochSecond()); + taskRepository.save(task); - return true; - } + return true; + } - private TaskDto toDto(Task t) - { - return new TaskDto( - t.getId(), - t.getName(), - t.getReward() / 100.0, - t.getAssigned(), - t.getSchedule()); - } + private TaskDto toDto(Task t) { + return new TaskDto( + t.getId(), + t.getName(), + t.getReward() / 100.0, + t.getAssigned(), + t.getSchedule()); + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TransferService.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TransferService.java index 7b81ba0..ab2e31a 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TransferService.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/TransferService.java @@ -9,94 +9,78 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Service -public class TransferService -{ - private final AllowanceRepository allowanceRepository; +public class TransferService { + private final AllowanceRepository allowanceRepository; - public TransferService(AllowanceRepository allowanceRepository) - { - this.allowanceRepository = allowanceRepository; - } + public TransferService(AllowanceRepository allowanceRepository) { + this.allowanceRepository = allowanceRepository; + } - @Transactional - public TransferResult transfer(TransferRequest request) - { - if (request.from() == request.to()) - { - return TransferResult.success(); - } + @Transactional + public TransferResult transfer(TransferRequest request) { + if (request.from() == request.to()) { + return TransferResult.success(); + } - int amountCents = (int) Math.round(request.amount() * 100.0); - if (amountCents <= 0) - { - return TransferResult.badRequest("amount must be positive"); - } + int amountCents = (int) Math.round(request.amount() * 100.0); + if (amountCents <= 0) { + return TransferResult.badRequest("amount must be positive"); + } - Optional fromOpt = allowanceRepository.findById(request.from()); - if (fromOpt.isEmpty()) - { - return TransferResult.notFound(); - } + Optional fromOpt = allowanceRepository.findById(request.from()); + if (fromOpt.isEmpty()) { + return TransferResult.notFound(); + } - Optional toOpt = allowanceRepository.findById(request.to()); - if (toOpt.isEmpty()) - { - return TransferResult.notFound(); - } + Optional toOpt = allowanceRepository.findById(request.to()); + if (toOpt.isEmpty()) { + return TransferResult.notFound(); + } - Allowance from = fromOpt.get(); - Allowance to = toOpt.get(); + Allowance from = fromOpt.get(); + Allowance to = toOpt.get(); - if (from.getUserId() != to.getUserId()) - { - return TransferResult.badRequest("Allowances do not belong to the same user"); - } + if (from.getUserId() != to.getUserId()) { + return TransferResult.badRequest("Allowances do not belong to the same user"); + } - long remainingTo = to.getTarget() - to.getBalance(); - if (remainingTo <= 0) - { - return TransferResult.badRequest("target already reached"); - } + long remainingTo = to.getTarget() - to.getBalance(); + if (remainingTo <= 0) { + return TransferResult.badRequest("target already reached"); + } - int transfer = amountCents; - if (transfer > remainingTo) - { - transfer = (int) remainingTo; - } + int transfer = amountCents; + if (transfer > remainingTo) { + transfer = (int) remainingTo; + } - if (from.getBalance() < transfer) - { - return TransferResult.badRequest("Insufficient funds in source allowance"); - } + if (from.getBalance() < transfer) { + return TransferResult.badRequest("Insufficient funds in source allowance"); + } - from.setBalance(from.getBalance() - transfer); - to.setBalance(to.getBalance() + transfer); - allowanceRepository.save(from); - allowanceRepository.save(to); + from.setBalance(from.getBalance() - transfer); + to.setBalance(to.getBalance() + transfer); + allowanceRepository.save(from); + allowanceRepository.save(to); - return TransferResult.success(); - } + return TransferResult.success(); + } - public record TransferResult(Status status, String message) - { - public enum Status - { - SUCCESS, BAD_REQUEST, NOT_FOUND - } + public record TransferResult(Status status, String message) { + public static TransferResult success() { + return new TransferResult(Status.SUCCESS, "Transfer successful"); + } - public static TransferResult success() - { - return new TransferResult(Status.SUCCESS, "Transfer successful"); - } + public static TransferResult badRequest(String message) { + return new TransferResult(Status.BAD_REQUEST, message); + } - public static TransferResult badRequest(String message) - { - return new TransferResult(Status.BAD_REQUEST, message); - } + public static TransferResult notFound() { + return new TransferResult(Status.NOT_FOUND, "Allowance not found"); + } - public static TransferResult notFound() - { - return new TransferResult(Status.NOT_FOUND, "Allowance not found"); - } - } + public enum Status { + SUCCESS, BAD_REQUEST, NOT_FOUND + } + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/UserService.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/UserService.java index ccc74cd..d729d05 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/UserService.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/service/UserService.java @@ -1,7 +1,7 @@ package be.seeseepuff.allowanceplanner.service; -import be.seeseepuff.allowanceplanner.dto.*; -import be.seeseepuff.allowanceplanner.entity.User; +import be.seeseepuff.allowanceplanner.dto.UserDto; +import be.seeseepuff.allowanceplanner.dto.UserWithAllowanceDto; import be.seeseepuff.allowanceplanner.repository.UserRepository; import org.springframework.stereotype.Service; @@ -9,34 +9,29 @@ import java.util.List; import java.util.Optional; @Service -public class UserService -{ - private final UserRepository userRepository; +public class UserService { + private final UserRepository userRepository; - public UserService(UserRepository userRepository) - { - this.userRepository = userRepository; - } + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } - public List getUsers() - { - return userRepository.findAll().stream() - .map(u -> new UserDto(u.getId(), u.getName())) - .toList(); - } + public List getUsers() { + return userRepository.findAll().stream() + .map(u -> new UserDto(u.getId(), u.getName())) + .toList(); + } - public Optional getUser(int userId) - { - return userRepository.findById(userId) - .map(u -> - { - long totalAmount = userRepository.sumHistoryAmount(userId); - return new UserWithAllowanceDto(u.getId(), u.getName(), totalAmount / 100.0); - }); - } + public Optional getUser(int userId) { + return userRepository.findById(userId) + .map(u -> + { + long totalAmount = userRepository.sumHistoryAmount(userId); + return new UserWithAllowanceDto(u.getId(), u.getName(), totalAmount / 100.0); + }); + } - public boolean userExists(int userId) - { - return userRepository.existsById(userId); - } + public boolean userExists(int userId) { + return userRepository.existsById(userId); + } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/util/ColourUtil.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/util/ColourUtil.java index 6fda514..eb8770b 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/util/ColourUtil.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/util/ColourUtil.java @@ -1,42 +1,33 @@ package be.seeseepuff.allowanceplanner.util; -public class ColourUtil -{ - private ColourUtil() - { - } +public class ColourUtil { + private ColourUtil() { + } - public static int convertStringToColour(String colourStr) - { - if (colourStr == null || colourStr.isEmpty()) - { - return 0xFF0000; // Default colour - } - if (colourStr.charAt(0) == '#') - { - colourStr = colourStr.substring(1); - } - if (colourStr.length() != 6 && colourStr.length() != 3) - { - throw new IllegalArgumentException("colour must be a valid hex string"); - } - int colour = Integer.parseInt(colourStr, 16); - if (colourStr.length() == 3) - { - int r = (colour & 0xF00) >> 8; - int g = (colour & 0x0F0) >> 4; - int b = (colour & 0x00F); - colour = (r << 20) | (g << 12) | (b << 4); - } - return colour; - } + public static int convertStringToColour(String colourStr) { + if (colourStr == null || colourStr.isEmpty()) { + return 0xFF0000; // Default colour + } + if (colourStr.charAt(0) == '#') { + colourStr = colourStr.substring(1); + } + if (colourStr.length() != 6 && colourStr.length() != 3) { + throw new IllegalArgumentException("colour must be a valid hex string"); + } + int colour = Integer.parseInt(colourStr, 16); + if (colourStr.length() == 3) { + int r = (colour & 0xF00) >> 8; + int g = (colour & 0x0F0) >> 4; + int b = (colour & 0x00F); + colour = (r << 20) | (g << 12) | (b << 4); + } + return colour; + } - public static String convertColourToString(Integer colour) - { - if (colour == null) - { - return ""; - } - return String.format("#%06X", colour); - } + public static String convertColourToString(Integer colour) { + if (colour == null) { + return ""; + } + return String.format("#%06X", colour); + } } diff --git a/backend-spring/src/main/resources/application.properties b/backend-spring/src/main/resources/application.properties index 35e78e6..e197cb9 100644 --- a/backend-spring/src/main/resources/application.properties +++ b/backend-spring/src/main/resources/application.properties @@ -1,12 +1,8 @@ spring.application.name=allowance-planner - spring.datasource.url=jdbc:postgresql://localhost:5432/allowance_planner spring.datasource.username=postgres spring.datasource.password=postgres - spring.jpa.hibernate.ddl-auto=validate spring.jpa.open-in-view=false - spring.flyway.enabled=true - server.port=8080 diff --git a/backend-spring/src/main/resources/templates/index.html b/backend-spring/src/main/resources/templates/index.html index a199188..b48962b 100644 --- a/backend-spring/src/main/resources/templates/index.html +++ b/backend-spring/src/main/resources/templates/index.html @@ -20,8 +20,8 @@

Users

- +
@@ -39,10 +39,10 @@ - + - - + + @@ -66,7 +66,7 @@

Tasks

-
+ @@ -91,10 +91,10 @@ - + - - + + diff --git a/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ApiTest.java b/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ApiTest.java index 1433976..40cf6da 100644 --- a/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ApiTest.java +++ b/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ApiTest.java @@ -2,18 +2,17 @@ package be.seeseepuff.allowanceplanner; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import org.flywaydb.core.Flyway; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; -import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; - -import org.flywaydb.core.Flyway; -import org.springframework.beans.factory.annotation.Autowired; +import org.testcontainers.postgresql.PostgreSQLContainer; import java.time.Instant; import java.util.List; @@ -24,1219 +23,1155 @@ import static org.hamcrest.Matchers.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers -class ApiTest -{ - private static final String TEST_HISTORY_NAME = "Test History"; - - @Container - static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:17") - .withDatabaseName("allowance_planner_test") - .withUsername("test") - .withPassword("test"); - - @LocalServerPort - int port; - - @Autowired - Flyway flyway; - - @DynamicPropertySource - static void configureProperties(DynamicPropertyRegistry registry) - { - registry.add("spring.datasource.url", postgres::getJdbcUrl); - registry.add("spring.datasource.username", postgres::getUsername); - registry.add("spring.datasource.password", postgres::getPassword); - registry.add("spring.flyway.clean-disabled", () -> "false"); - } - - @BeforeEach - void setUp() - { - RestAssured.port = port; - RestAssured.basePath = "/api"; - RestAssured.config = RestAssured.config() - .jsonConfig(io.restassured.config.JsonConfig.jsonConfig() - .numberReturnType(io.restassured.path.json.config.JsonPathConfig.NumberReturnType.DOUBLE)); - - // Clean and re-migrate the database before each test - flyway.clean(); - flyway.migrate(); - } - - // ---- User Tests ---- - - @Test - void getUsers() - { - given() - .when() - .get("/users") - .then() - .statusCode(200) - .body("$.size()", is(2)) - .body("[0].name", isA(String.class)) - .body("[1].name", isA(String.class)); - } - - @Test - void getUser() - { - given() - .when() - .get("/user/1") - .then() - .statusCode(200) - .body("name", is("Seeseemelk")) - .body("id", is(1)) - .body("allowance", is(0.0d)); - } - - @Test - void getUserUnknown() - { - given() - .when() - .get("/user/999") - .then() - .statusCode(404); - } - - @Test - void getUserBadId() - { - given() - .when() - .get("/user/bad-id") - .then() - .statusCode(400); - } - - // ---- Allowance Tests ---- - - @Test - void getUserAllowanceWhenNoAllowancePresent() - { - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("$.size()", is(1)) - .body("[0].id", is(0)); - } - - @Test - void getUserAllowance() - { - createAllowance(1, TEST_HISTORY_NAME, 5000, 10); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("$.size()", is(2)) - .body("[1].name", is(TEST_HISTORY_NAME)) - .body("[1].target", is(5000.0d)) - .body("[1].weight", is(10.0d)) - .body("[1].progress", is(0.0d)) - .body("[1]", not(hasKey("user_id"))); - } - - @Test - void getUserAllowanceNoUser() - { - given() - .when() - .get("/user/999/allowance") - .then() - .statusCode(404); - } - - @Test - void getUserAllowanceBadId() - { - given() - .when() - .get("/user/bad-id/allowance") - .then() - .statusCode(400); - } - - @Test - void createUserAllowance() - { - int allowanceId = createAllowance(1, TEST_HISTORY_NAME, 5000, 10); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("$.size()", is(2)) - .body("[1].id", is(allowanceId)) - .body("[1].name", is(TEST_HISTORY_NAME)) - .body("[1].target", is(5000.0d)) - .body("[1].weight", is(10.0d)) - .body("[1].progress", is(0.0d)); - } - - @Test - void createUserAllowanceNoUser() - { - given() - .contentType(ContentType.JSON) - .body(Map.of("name", TEST_HISTORY_NAME, "target", 5000, "weight", 10)) - .when() - .post("/user/999/allowance") - .then() - .statusCode(404); - } - - @Test - void createUserAllowanceInvalidInput() - { - // Empty name - given() - .contentType(ContentType.JSON) - .body(Map.of("name", "", "target", 5000, "weight", 10)) - .when() - .post("/user/1/allowance") - .then() - .statusCode(400); - - // Missing name - given() - .contentType(ContentType.JSON) - .body(Map.of("target", 5000)) - .when() - .post("/user/1/allowance") - .then() - .statusCode(400); - } - - @Test - void createUserAllowanceBadId() - { - given() - .contentType(ContentType.JSON) - .body(Map.of("name", TEST_HISTORY_NAME, "target", 5000, "weight", 10)) - .when() - .post("/user/bad-id/allowance") - .then() - .statusCode(400); - } - - @Test - void deleteUserAllowance() - { - int allowanceId = createAllowance(1, TEST_HISTORY_NAME, 1000, 5); - - given() - .when() - .delete("/user/1/allowance/" + allowanceId) - .then() - .statusCode(200) - .body("message", is("History deleted successfully")); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("$.size()", is(1)); - } - - @Test - void deleteUserRestAllowance() - { - given() - .when() - .delete("/user/1/allowance/0") - .then() - .statusCode(400); - } - - @Test - void deleteUserAllowanceNotFound() - { - given() - .when() - .delete("/user/1/allowance/999") - .then() - .statusCode(404) - .body("error", is("History not found")); - } - - @Test - void deleteUserAllowanceInvalidId() - { - given() - .when() - .delete("/user/1/allowance/invalid-id") - .then() - .statusCode(400) - .body("error", is("Invalid allowance ID")); - } - - // ---- Task Tests ---- - - @Test - void createTask() - { - // Without assigned user - int taskId = given() - .contentType(ContentType.JSON) - .body(Map.of("name", "Test Task", "reward", 100)) - .when() - .post("/tasks") - .then() - .statusCode(201) - .body("id", notNullValue()) - .extract().path("id"); - - given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); - - given() - .when() - .get("/task/" + taskId) - .then() - .statusCode(200) - .body("id", is(taskId)) - .body("name", is("Test Task")) - .body("reward", is(100.0d)) - .body("assigned", nullValue()); - - // With assigned user - given() - .contentType(ContentType.JSON) - .body(Map.of("name", "Test Task Assigned", "reward", 200, "assigned", 1)) - .when() - .post("/tasks") - .then() - .statusCode(201) - .body("id", notNullValue()); - } - - @Test - void deleteTask() - { - int taskId = createTestTask(100); - - given().when().delete("/task/" + taskId).then().statusCode(200); - given().when().get("/task/" + taskId).then().statusCode(404); - } - - @Test - void deleteTaskNotFound() - { - given().when().delete("/task/1").then().statusCode(404); - } - - @Test - void createTaskNoName() - { - given() - .contentType(ContentType.JSON) - .body(Map.of("reward", 100)) - .when() - .post("/tasks") - .then() - .statusCode(400); - } - - @Test - void createTaskInvalidAssignedUser() - { - given() - .contentType(ContentType.JSON) - .body(Map.of("name", "Test Task Invalid User", "reward", 100, "assigned", 999)) - .when() - .post("/tasks") - .then() - .statusCode(404) - .body("error", is("User not found")); - } - - @Test - void createTaskInvalidRequestBody() - { - given() - .contentType(ContentType.JSON) - .body(Map.of("reward", 5000)) - .when() - .post("/tasks") - .then() - .statusCode(400); - } - - @Test - void getTaskWhenNoTasks() - { - given() - .when() - .get("/tasks") - .then() - .statusCode(200) - .body("$.size()", is(0)); - } - - @Test - void getTasksWhenTasks() - { - createTestTask(100); - - given() - .when() - .get("/tasks") - .then() - .statusCode(200) - .body("$.size()", is(1)) - .body("[0].name", is("Test Task")) - .body("[0].reward", is(100.0d)) - .body("[0].assigned", nullValue()); - } - - @Test - void getTask() - { - int taskId = createTestTask(100); - - given() - .when() - .get("/task/" + taskId) - .then() - .statusCode(200) - .body("id", is(taskId)) - .body("name", is("Test Task")) - .body("reward", is(100.0d)) - .body("assigned", nullValue()); - } - - @Test - void getTaskInvalidId() - { - createTestTask(100); - // Task ID won't be found since we use auto-increment and there's only one - given().when().get("/task/99999").then().statusCode(404); - } - - @Test - void getTaskBadId() - { - createTestTask(100); - given().when().get("/task/invalid").then().statusCode(400); - } - - @Test - void putTaskModifiesTask() - { - int taskId = createTestTask(100); - - given() - .contentType(ContentType.JSON) - .body(Map.of("name", "Updated Task", "reward", 100)) - .when() - .put("/task/" + taskId) - .then() - .statusCode(200); - - given() - .when() - .get("/task/" + taskId) - .then() - .statusCode(200) - .body("id", is(taskId)) - .body("name", is("Updated Task")) - .body("reward", is(100.0d)); - } - - @Test - void putTaskInvalidTaskId() - { - createTestTask(100); - - given() - .contentType(ContentType.JSON) - .body(Map.of("name", "Updated Task")) - .when() - .put("/task/999") - .then() - .statusCode(404); - } - - // ---- History Tests ---- - - @Test - void postHistory() - { - given() - .contentType(ContentType.JSON) - .body(Map.of("allowance", 100, "description", "Add a 100")) - .when() - .post("/user/1/history") - .then() - .statusCode(200); - - given() - .contentType(ContentType.JSON) - .body(Map.of("allowance", 20, "description", "Lolol")) - .when() - .post("/user/1/history") - .then() - .statusCode(200); - - given() - .contentType(ContentType.JSON) - .body(Map.of("allowance", -10, "description", "Subtracting")) - .when() - .post("/user/1/history") - .then() - .statusCode(200); - - given() - .when() - .get("/user/1") - .then() - .statusCode(200) - .body("allowance", is(110.0d)); - } - - @Test - void postHistoryInvalidUserId() - { - given() - .contentType(ContentType.JSON) - .body(Map.of("allowance", 100, "description", "Good")) - .when() - .post("/user/999/history") - .then() - .statusCode(404); - } - - @Test - void postHistoryInvalidDescription() - { - given() - .contentType(ContentType.JSON) - .body(Map.of("allowance", 100)) - .when() - .post("/user/1/history") - .then() - .statusCode(400); - } - - @Test - void getHistory() - { - Instant before = Instant.now().minusSeconds(2); - Instant after = Instant.now().plusSeconds(2); - - given() - .contentType(ContentType.JSON) - .body(Map.of("allowance", 100, "description", "Add 100")) - .when() - .post("/user/1/history") - .then() - .statusCode(200); - given() - .contentType(ContentType.JSON) - .body(Map.of("allowance", 20, "description", "Add 20")) - .when() - .post("/user/1/history") - .then() - .statusCode(200); - given() - .contentType(ContentType.JSON) - .body(Map.of("allowance", -10, "description", "Subtract 10")) - .when() - .post("/user/1/history") - .then() - .statusCode(200); - - // History is returned newest first (by ID desc) - given() - .when() - .get("/user/1/history") - .then() - .statusCode(200) - .body("$.size()", is(3)) - .body("[0].allowance", is(-10.0d)) - .body("[0].description", is("Subtract 10")) - .body("[1].allowance", is(20.0d)) - .body("[1].description", is("Add 20")) - .body("[2].allowance", is(100.0d)) - .body("[2].description", is("Add 100")); - } - - // ---- Allowance By ID Tests ---- - - @Test - void getUserAllowanceById() - { - int allowanceId = createAllowanceWithColour(1, TEST_HISTORY_NAME, 5000, 10, "#FF5733"); - - given() - .when() - .get("/user/1/allowance/" + allowanceId) - .then() - .statusCode(200) - .body("id", is(allowanceId)) - .body("name", is(TEST_HISTORY_NAME)) - .body("target", is(5000.0d)) - .body("weight", is(10.0d)) - .body("progress", is(0.0d)) - .body("colour", is("#FF5733")); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("$.size()", is(2)) - .body("[1].id", is(allowanceId)) - .body("[1].colour", is("#FF5733")); - } - - @Test - void getUserByAllowanceIdInvalidAllowance() - { - given().when().get("/user/1/allowance/9999").then().statusCode(404); - } - - @Test - void getUserByAllowanceByIdInvalidUserId() - { - given().when().get("/user/999/allowance/1").then().statusCode(404); - } - - @Test - void getUserByAllowanceByIdBadUserId() - { - given().when().get("/user/bad/allowance/1").then().statusCode(400); - } - - @Test - void getUserByAllowanceByIdBadAllowanceId() - { - given().when().get("/user/1/allowance/bad").then().statusCode(400); - } - - @Test - void putAllowanceById() - { - int allowanceId = createAllowanceWithColour(1, TEST_HISTORY_NAME, 5000, 10, "#FF5733"); - - given() - .contentType(ContentType.JSON) - .body(Map.of("name", "Updated Allowance", "target", 6000, "weight", 15, "colour", "#3357FF")) - .when() - .put("/user/1/allowance/" + allowanceId) - .then() - .statusCode(200); - - given() - .when() - .get("/user/1/allowance/" + allowanceId) - .then() - .statusCode(200) - .body("id", is(allowanceId)) - .body("name", is("Updated Allowance")) - .body("target", is(6000.0d)) - .body("weight", is(15.0d)) - .body("colour", is("#3357FF")); - } - - // ---- Complete Task Tests ---- - - @Test - void completeTask() - { - int taskId = createTestTask(101); - - given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); - - // Update rest allowance weight - given() - .contentType(ContentType.JSON) - .body(Map.of("name", "", "target", 0, "weight", 25, "colour", "")) - .when() - .put("/user/1/allowance/0") - .then() - .statusCode(200); - - // Create two allowance goals - createAllowance(1, "Test Allowance 1", 100, 50); - createAllowance(1, "Test Allowance 1", 10, 25); - - // Complete the task - given().when().post("/task/" + taskId + "/complete").then().statusCode(200); - - // Verify task is completed - given().when().get("/task/" + taskId).then().statusCode(404); - - // Verify allowances for user 1 - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("$.size()", is(3)) - .body("[0].id", is(0)) - .body("[0].progress", closeTo(30.34, 0.01)) - .body("[1].progress", closeTo(60.66, 0.01)) - .body("[2].progress", closeTo(10.0, 0.01)); - - // Verify allowances for user 2 - given() - .when() - .get("/user/2/allowance") - .then() - .statusCode(200) - .body("$.size()", is(1)) - .body("[0].id", is(0)) - .body("[0].progress", closeTo(101.0, 0.01)); - - // Verify history for both users - for (int userId = 1; userId <= 2; userId++) - { - given() - .when() - .get("/user/" + userId + "/history") - .then() - .statusCode(200) - .body("$.size()", is(1)) - .body("[0].allowance", closeTo(101.0, 0.01)); - } - } - - @Test - void completeTaskWithNoWeights() - { - int taskId = createTestTask(101); - - given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); - - // Ensure main allowance has no weight - given() - .contentType(ContentType.JSON) - .body(Map.of("name", "", "target", 0, "weight", 0, "colour", "")) - .when() - .put("/user/1/allowance/0") - .then() - .statusCode(200); - - // Complete the task - given().when().post("/task/" + taskId + "/complete").then().statusCode(200); - - // Verify task is completed - given().when().get("/task/" + taskId).then().statusCode(404); - - // Verify allowances for user 1 - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("$.size()", is(1)) - .body("[0].id", is(0)) - .body("[0].progress", closeTo(101.0, 0.01)); - - // Verify allowances for user 2 - given() - .when() - .get("/user/2/allowance") - .then() - .statusCode(200) - .body("$.size()", is(1)) - .body("[0].id", is(0)) - .body("[0].progress", closeTo(101.0, 0.01)); - } - - @Test - void completeTaskAllowanceWeightsSumTo0() - { - int taskId = createTestTask(101); - - given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); - - // Update rest allowance to 0 weight - given() - .contentType(ContentType.JSON) - .body(Map.of("name", "", "target", 0, "weight", 0, "colour", "")) - .when() - .put("/user/1/allowance/0") - .then() - .statusCode(200); - - // Create allowance with 0 weight - createAllowance(1, "Test Allowance 1", 1000, 0); - - // Complete the task - given().when().post("/task/" + taskId + "/complete").then().statusCode(200); - - // Verify allowances for user 1 - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("$.size()", is(2)) - .body("[0].id", is(0)) - .body("[0].progress", closeTo(101.0, 0.01)) - .body("[1].progress", closeTo(0.0, 0.01)); - } - - @Test - void completeTaskInvalidId() - { - given().when().post("/task/999/complete").then().statusCode(404); - } - - // ---- Complete Allowance Tests ---- - - @Test - void completeAllowance() - { - createTestTask(100); - createAllowance(1, "Test Allowance 1", 100, 50); - - // Update base allowance to 0 weight - given() - .contentType(ContentType.JSON) - .body(Map.of("name", "", "target", 0, "weight", 0, "colour", "")) - .when() - .put("/user/1/allowance/0") - .then() - .statusCode(200); - - // Complete the task - given().when().post("/task/1/complete").then().statusCode(200); - - // Get the allowance ID (first named allowance) - int allowanceId = given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .extract() - .path("[1].id"); - - // Complete allowance - given().when().post("/user/1/allowance/" + allowanceId + "/complete").then().statusCode(200); - - // Verify allowance no longer exists - given().when().get("/user/1/allowance/" + allowanceId).then().statusCode(404); - - // Verify history (newest first) - given() - .when() - .get("/user/1/history") - .then() - .statusCode(200) - .body("$.size()", is(2)) - .body("[0].allowance", closeTo(-100.0, 0.01)) - .body("[0].description", is("Allowance completed: Test Allowance 1")) - .body("[1].allowance", closeTo(100.0, 0.01)) - .body("[1].description", is("Task completed: Test Task")); - } - - @Test - void completeAllowanceInvalidUserId() - { - given().when().post("/user/999/allowance/1/complete").then().statusCode(404); - } - - @Test - void completeAllowanceInvalidAllowanceId() - { - given().when().post("/user/1/allowance/999/complete").then().statusCode(404); - } - - // ---- Bulk Update Tests ---- - - @Test - void putBulkAllowance() - { - int id1 = createAllowance(1, "Test Allowance 1", 1000, 1); - int id2 = createAllowance(1, "Test Allowance 2", 1000, 2); - - given() - .contentType(ContentType.JSON) - .body(List.of( - Map.of("id", id1, "weight", 5), - Map.of("id", 0, "weight", 99), - Map.of("id", id2, "weight", 10))) - .when() - .put("/user/1/allowance") - .then() - .statusCode(200); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("$.size()", is(3)) - .body("[0].id", is(0)) - .body("[0].weight", closeTo(99.0, 0.01)) - .body("[1].id", is(id1)) - .body("[1].weight", closeTo(5.0, 0.01)) - .body("[2].id", is(id2)) - .body("[2].weight", closeTo(10.0, 0.01)); - } - - // ---- Add Allowance Amount Tests ---- - - @Test - void addAllowanceSimple() - { - int allowanceId = createAllowance(1, "Test Allowance 1", 1000, 1); - - given() - .contentType(ContentType.JSON) - .body(Map.of("amount", 10, "description", "Added to allowance 1")) - .when() - .post("/user/1/allowance/" + allowanceId + "/add") - .then() - .statusCode(200); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("[1].id", is(allowanceId)) - .body("[1].progress", closeTo(10.0, 0.01)); - - given() - .when() - .get("/user/1/history") - .then() - .statusCode(200) - .body("$.size()", is(1)) - .body("[0].allowance", closeTo(10.0, 0.01)) - .body("[0].description", is("Added to allowance 1")); - } - - @Test - void addAllowanceWithSpillage() - { - int id1 = createAllowance(1, "Test Allowance 1", 5, 1); - int id2 = createAllowance(1, "Test Allowance 2", 5, 1); - given() - .contentType(ContentType.JSON) - .body(Map.of("name", "", "target", 0, "weight", 1, "colour", "")) - .when() - .put("/user/1/allowance/0") - .then() - .statusCode(200); - - given() - .contentType(ContentType.JSON) - .body(Map.of("amount", 10, "description", "Added to allowance 1")) - .when() - .post("/user/1/allowance/" + id1 + "/add") - .then() - .statusCode(200); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("[1].id", is(id1)) - .body("[1].progress", closeTo(5.0, 0.01)) - .body("[2].id", is(id2)) - .body("[2].progress", closeTo(2.5, 0.01)) - .body("[0].id", is(0)) - .body("[0].progress", closeTo(2.5, 0.01)); - - given() - .when() - .get("/user/1/history") - .then() - .statusCode(200) - .body("$.size()", is(1)) - .body("[0].allowance", closeTo(10.0, 0.01)) - .body("[0].description", is("Added to allowance 1")); - } - - @Test - void addAllowanceIdZero() - { - createAllowance(1, "Test Allowance 1", 1000, 1); - - given() - .contentType(ContentType.JSON) - .body(Map.of("amount", 10, "description", "Added to allowance 1")) - .when() - .post("/user/1/allowance/0/add") - .then() - .statusCode(200); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("[0].id", is(0)) - .body("[0].progress", closeTo(10.0, 0.01)); - - given() - .when() - .get("/user/1/history") - .then() - .statusCode(200) - .body("$.size()", is(1)) - .body("[0].allowance", closeTo(10.0, 0.01)) - .body("[0].description", is("Added to allowance 1")); - } - - @Test - void subtractAllowanceSimple() - { - int allowanceId = createAllowance(1, "Test Allowance 1", 1000, 1); - - given() - .contentType(ContentType.JSON) - .body(Map.of("amount", 10, "description", "Added to allowance 1")) - .when() - .post("/user/1/allowance/" + allowanceId + "/add") - .then() - .statusCode(200); - - given() - .contentType(ContentType.JSON) - .body(Map.of("amount", -2.5, "description", "Added to allowance 1")) - .when() - .post("/user/1/allowance/" + allowanceId + "/add") - .then() - .statusCode(200); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("[1].id", is(allowanceId)) - .body("[1].progress", closeTo(7.5, 0.01)); - - given() - .when() - .get("/user/1/history") - .then() - .statusCode(200) - .body("$.size()", is(2)) - .body("[0].allowance", closeTo(-2.5, 0.01)) - .body("[0].description", is("Added to allowance 1")) - .body("[1].allowance", closeTo(10.0, 0.01)) - .body("[1].description", is("Added to allowance 1")); - } - - @Test - void subtractAllowanceIdZero() - { - createAllowance(1, "Test Allowance 1", 1000, 1); - - given() - .contentType(ContentType.JSON) - .body(Map.of("amount", 10, "description", "Added to allowance 1")) - .when() - .post("/user/1/allowance/0/add") - .then() - .statusCode(200); - - given() - .contentType(ContentType.JSON) - .body(Map.of("amount", -2.5, "description", "Added to allowance 1")) - .when() - .post("/user/1/allowance/0/add") - .then() - .statusCode(200); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("[0].id", is(0)) - .body("[0].progress", closeTo(7.5, 0.01)); - - given() - .when() - .get("/user/1/history") - .then() - .statusCode(200) - .body("$.size()", is(2)) - .body("[0].allowance", closeTo(-2.5, 0.01)) - .body("[0].description", is("Added to allowance 1")) - .body("[1].allowance", closeTo(10.0, 0.01)) - .body("[1].description", is("Added to allowance 1")); - } - - // ---- Transfer Tests ---- - - @Test - void transferSuccessful() - { - int id1 = createAllowance(1, "From Allowance", 100, 1); - int id2 = createAllowance(1, "To Allowance", 100, 1); - - // Add 30 to allowance 1 - given() - .contentType(ContentType.JSON) - .body(Map.of("amount", 30, "description", "funds")) - .when() - .post("/user/1/allowance/" + id1 + "/add") - .then() - .statusCode(200); - - // Transfer 10 from 1 to 2 - given() - .contentType(ContentType.JSON) - .body(Map.of("from", id1, "to", id2, "amount", 10)) - .when() - .post("/transfer") - .then() - .statusCode(200) - .body("message", is("Transfer successful")); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("[1].progress", closeTo(20.0, 0.01)) - .body("[2].progress", closeTo(10.0, 0.01)); - } - - @Test - void transferCapsAtTarget() - { - int id1 = createAllowance(1, "From Allowance", 100, 1); - int id2 = createAllowance(1, "To Allowance", 5, 1); - - // Add 10 to allowance 1 - given() - .contentType(ContentType.JSON) - .body(Map.of("amount", 10, "description", "funds")) - .when() - .post("/user/1/allowance/" + id1 + "/add") - .then() - .statusCode(200); - - // Transfer 10, but to only needs 5 - given() - .contentType(ContentType.JSON) - .body(Map.of("from", id1, "to", id2, "amount", 10)) - .when() - .post("/transfer") - .then() - .statusCode(200); - - given() - .when() - .get("/user/1/allowance") - .then() - .statusCode(200) - .body("[1].progress", closeTo(5.0, 0.01)) - .body("[2].progress", closeTo(5.0, 0.01)); - } - - @Test - void transferDifferentUsersFails() - { - int id1 = createAllowance(1, "User1 Allowance", 100, 1); - - // Create allowance for user 2 - int id2 = createAllowance(2, "User2 Allowance", 100, 1); - - // Add to user1 allowance - given() - .contentType(ContentType.JSON) - .body(Map.of("amount", 10, "description", "funds")) - .when() - .post("/user/1/allowance/" + id1 + "/add") - .then() - .statusCode(200); - - // Transfer between different users - given() - .contentType(ContentType.JSON) - .body(Map.of("from", id1, "to", id2, "amount", 5)) - .when() - .post("/transfer") - .then() - .statusCode(400); - } - - @Test - void transferInsufficientFunds() - { - int id1 = createAllowance(1, "From Allowance", 100, 1); - int id2 = createAllowance(1, "To Allowance", 100, 1); - - given() - .contentType(ContentType.JSON) - .body(Map.of("from", id1, "to", id2, "amount", 10)) - .when() - .post("/transfer") - .then() - .statusCode(400) - .body("error", containsStringIgnoringCase("insufficient")); - } - - @Test - void transferNotFound() - { - given() - .contentType(ContentType.JSON) - .body(Map.of("from", 999, "to", 1000, "amount", 1)) - .when() - .post("/transfer") - .then() - .statusCode(404); - } - - // ---- Helpers ---- - - private int createTestTask(int reward) - { - return given() - .contentType(ContentType.JSON) - .body(Map.of("name", "Test Task", "reward", reward)) - .when() - .post("/tasks") - .then() - .statusCode(201) - .extract() - .path("id"); - } - - private int createAllowance(int userId, String name, double target, double weight) - { - return given() - .contentType(ContentType.JSON) - .body(Map.of("name", name, "target", target, "weight", weight)) - .when() - .post("/user/" + userId + "/allowance") - .then() - .statusCode(201) - .extract() - .path("id"); - } - - private int createAllowanceWithColour(int userId, String name, double target, double weight, String colour) - { - return given() - .contentType(ContentType.JSON) - .body(Map.of("name", name, "target", target, "weight", weight, "colour", colour)) - .when() - .post("/user/" + userId + "/allowance") - .then() - .statusCode(201) - .extract() - .path("id"); - } +class ApiTest { + private static final String TEST_HISTORY_NAME = "Test History"; + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:18") + .withDatabaseName("allowance_planner_test") + .withUsername("test") + .withPassword("test"); + + @LocalServerPort + int port; + + @Autowired + Flyway flyway; + + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", postgres::getJdbcUrl); + registry.add("spring.datasource.username", postgres::getUsername); + registry.add("spring.datasource.password", postgres::getPassword); + registry.add("spring.flyway.clean-disabled", () -> "false"); + } + + @BeforeEach + void setUp() { + RestAssured.port = port; + RestAssured.basePath = "/api"; + RestAssured.config = RestAssured.config() + .jsonConfig(io.restassured.config.JsonConfig.jsonConfig() + .numberReturnType(io.restassured.path.json.config.JsonPathConfig.NumberReturnType.DOUBLE)); + + // Clean and re-migrate the database before each test + flyway.clean(); + flyway.migrate(); + } + + // ---- User Tests ---- + + @Test + void getUsers() { + given() + .when() + .get("/users") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].name", isA(String.class)) + .body("[1].name", isA(String.class)); + } + + @Test + void getUser() { + given() + .when() + .get("/user/1") + .then() + .statusCode(200) + .body("name", is("Seeseemelk")) + .body("id", is(1)) + .body("allowance", is(0.0d)); + } + + @Test + void getUserUnknown() { + given() + .when() + .get("/user/999") + .then() + .statusCode(404); + } + + @Test + void getUserBadId() { + given() + .when() + .get("/user/bad-id") + .then() + .statusCode(400); + } + + // ---- Allowance Tests ---- + + @Test + void getUserAllowanceWhenNoAllowancePresent() { + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].id", is(0)); + } + + @Test + void getUserAllowance() { + createAllowance(1, TEST_HISTORY_NAME, 5000, 10); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[1].name", is(TEST_HISTORY_NAME)) + .body("[1].target", is(5000.0d)) + .body("[1].weight", is(10.0d)) + .body("[1].progress", is(0.0d)) + .body("[1]", not(hasKey("user_id"))); + } + + @Test + void getUserAllowanceNoUser() { + given() + .when() + .get("/user/999/allowance") + .then() + .statusCode(404); + } + + @Test + void getUserAllowanceBadId() { + given() + .when() + .get("/user/bad-id/allowance") + .then() + .statusCode(400); + } + + @Test + void createUserAllowance() { + int allowanceId = createAllowance(1, TEST_HISTORY_NAME, 5000, 10); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[1].id", is(allowanceId)) + .body("[1].name", is(TEST_HISTORY_NAME)) + .body("[1].target", is(5000.0d)) + .body("[1].weight", is(10.0d)) + .body("[1].progress", is(0.0d)); + } + + @Test + void createUserAllowanceNoUser() { + given() + .contentType(ContentType.JSON) + .body(Map.of("name", TEST_HISTORY_NAME, "target", 5000, "weight", 10)) + .when() + .post("/user/999/allowance") + .then() + .statusCode(404); + } + + @Test + void createUserAllowanceInvalidInput() { + // Empty name + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 5000, "weight", 10)) + .when() + .post("/user/1/allowance") + .then() + .statusCode(400); + + // Missing name + given() + .contentType(ContentType.JSON) + .body(Map.of("target", 5000)) + .when() + .post("/user/1/allowance") + .then() + .statusCode(400); + } + + @Test + void createUserAllowanceBadId() { + given() + .contentType(ContentType.JSON) + .body(Map.of("name", TEST_HISTORY_NAME, "target", 5000, "weight", 10)) + .when() + .post("/user/bad-id/allowance") + .then() + .statusCode(400); + } + + @Test + void deleteUserAllowance() { + int allowanceId = createAllowance(1, TEST_HISTORY_NAME, 1000, 5); + + given() + .when() + .delete("/user/1/allowance/" + allowanceId) + .then() + .statusCode(200) + .body("message", is("History deleted successfully")); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(1)); + } + + @Test + void deleteUserRestAllowance() { + given() + .when() + .delete("/user/1/allowance/0") + .then() + .statusCode(400); + } + + @Test + void deleteUserAllowanceNotFound() { + given() + .when() + .delete("/user/1/allowance/999") + .then() + .statusCode(404) + .body("error", is("History not found")); + } + + @Test + void deleteUserAllowanceInvalidId() { + given() + .when() + .delete("/user/1/allowance/invalid-id") + .then() + .statusCode(400) + .body("error", is("Invalid allowance ID")); + } + + // ---- Task Tests ---- + + @Test + void createTask() { + // Without assigned user + int taskId = given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Test Task", "reward", 100)) + .when() + .post("/tasks") + .then() + .statusCode(201) + .body("id", notNullValue()) + .extract().path("id"); + + given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); + + given() + .when() + .get("/task/" + taskId) + .then() + .statusCode(200) + .body("id", is(taskId)) + .body("name", is("Test Task")) + .body("reward", is(100.0d)) + .body("assigned", nullValue()); + + // With assigned user + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Test Task Assigned", "reward", 200, "assigned", 1)) + .when() + .post("/tasks") + .then() + .statusCode(201) + .body("id", notNullValue()); + } + + @Test + void deleteTask() { + int taskId = createTestTask(100); + + given().when().delete("/task/" + taskId).then().statusCode(200); + given().when().get("/task/" + taskId).then().statusCode(404); + } + + @Test + void deleteTaskNotFound() { + given().when().delete("/task/1").then().statusCode(404); + } + + @Test + void createTaskNoName() { + given() + .contentType(ContentType.JSON) + .body(Map.of("reward", 100)) + .when() + .post("/tasks") + .then() + .statusCode(400); + } + + @Test + void createTaskInvalidAssignedUser() { + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Test Task Invalid User", "reward", 100, "assigned", 999)) + .when() + .post("/tasks") + .then() + .statusCode(404) + .body("error", is("User not found")); + } + + @Test + void createTaskInvalidRequestBody() { + given() + .contentType(ContentType.JSON) + .body(Map.of("reward", 5000)) + .when() + .post("/tasks") + .then() + .statusCode(400); + } + + @Test + void getTaskWhenNoTasks() { + given() + .when() + .get("/tasks") + .then() + .statusCode(200) + .body("$.size()", is(0)); + } + + @Test + void getTasksWhenTasks() { + createTestTask(100); + + given() + .when() + .get("/tasks") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].name", is("Test Task")) + .body("[0].reward", is(100.0d)) + .body("[0].assigned", nullValue()); + } + + @Test + void getTask() { + int taskId = createTestTask(100); + + given() + .when() + .get("/task/" + taskId) + .then() + .statusCode(200) + .body("id", is(taskId)) + .body("name", is("Test Task")) + .body("reward", is(100.0d)) + .body("assigned", nullValue()); + } + + @Test + void getTaskInvalidId() { + createTestTask(100); + // Task ID won't be found since we use auto-increment and there's only one + given().when().get("/task/99999").then().statusCode(404); + } + + @Test + void getTaskBadId() { + createTestTask(100); + given().when().get("/task/invalid").then().statusCode(400); + } + + @Test + void putTaskModifiesTask() { + int taskId = createTestTask(100); + + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Updated Task", "reward", 100)) + .when() + .put("/task/" + taskId) + .then() + .statusCode(200); + + given() + .when() + .get("/task/" + taskId) + .then() + .statusCode(200) + .body("id", is(taskId)) + .body("name", is("Updated Task")) + .body("reward", is(100.0d)); + } + + @Test + void putTaskInvalidTaskId() { + createTestTask(100); + + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Updated Task")) + .when() + .put("/task/999") + .then() + .statusCode(404); + } + + // ---- History Tests ---- + + @Test + void postHistory() { + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 100, "description", "Add a 100")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 20, "description", "Lolol")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", -10, "description", "Subtracting")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1") + .then() + .statusCode(200) + .body("allowance", is(110.0d)); + } + + @Test + void postHistoryInvalidUserId() { + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 100, "description", "Good")) + .when() + .post("/user/999/history") + .then() + .statusCode(404); + } + + @Test + void postHistoryInvalidDescription() { + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 100)) + .when() + .post("/user/1/history") + .then() + .statusCode(400); + } + + @Test + void getHistory() { + Instant before = Instant.now().minusSeconds(2); + Instant after = Instant.now().plusSeconds(2); + + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 100, "description", "Add 100")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", 20, "description", "Add 20")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + given() + .contentType(ContentType.JSON) + .body(Map.of("allowance", -10, "description", "Subtract 10")) + .when() + .post("/user/1/history") + .then() + .statusCode(200); + + // History is returned newest first (by ID desc) + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(3)) + .body("[0].allowance", is(-10.0d)) + .body("[0].description", is("Subtract 10")) + .body("[1].allowance", is(20.0d)) + .body("[1].description", is("Add 20")) + .body("[2].allowance", is(100.0d)) + .body("[2].description", is("Add 100")); + } + + // ---- Allowance By ID Tests ---- + + @Test + void getUserAllowanceById() { + int allowanceId = createAllowanceWithColour(1, TEST_HISTORY_NAME, 5000, 10, "#FF5733"); + + given() + .when() + .get("/user/1/allowance/" + allowanceId) + .then() + .statusCode(200) + .body("id", is(allowanceId)) + .body("name", is(TEST_HISTORY_NAME)) + .body("target", is(5000.0d)) + .body("weight", is(10.0d)) + .body("progress", is(0.0d)) + .body("colour", is("#FF5733")); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[1].id", is(allowanceId)) + .body("[1].colour", is("#FF5733")); + } + + @Test + void getUserByAllowanceIdInvalidAllowance() { + given().when().get("/user/1/allowance/9999").then().statusCode(404); + } + + @Test + void getUserByAllowanceByIdInvalidUserId() { + given().when().get("/user/999/allowance/1").then().statusCode(404); + } + + @Test + void getUserByAllowanceByIdBadUserId() { + given().when().get("/user/bad/allowance/1").then().statusCode(400); + } + + @Test + void getUserByAllowanceByIdBadAllowanceId() { + given().when().get("/user/1/allowance/bad").then().statusCode(400); + } + + @Test + void putAllowanceById() { + int allowanceId = createAllowanceWithColour(1, TEST_HISTORY_NAME, 5000, 10, "#FF5733"); + + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Updated Allowance", "target", 6000, "weight", 15, "colour", "#3357FF")) + .when() + .put("/user/1/allowance/" + allowanceId) + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance/" + allowanceId) + .then() + .statusCode(200) + .body("id", is(allowanceId)) + .body("name", is("Updated Allowance")) + .body("target", is(6000.0d)) + .body("weight", is(15.0d)) + .body("colour", is("#3357FF")); + } + + // ---- Complete Task Tests ---- + + @Test + void completeTask() { + int taskId = createTestTask(101); + + given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); + + // Update rest allowance weight + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 0, "weight", 25, "colour", "")) + .when() + .put("/user/1/allowance/0") + .then() + .statusCode(200); + + // Create two allowance goals + createAllowance(1, "Test Allowance 1", 100, 50); + createAllowance(1, "Test Allowance 1", 10, 25); + + // Complete the task + given().when().post("/task/" + taskId + "/complete").then().statusCode(200); + + // Verify task is completed + given().when().get("/task/" + taskId).then().statusCode(404); + + // Verify allowances for user 1 + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(3)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(30.34, 0.01)) + .body("[1].progress", closeTo(60.66, 0.01)) + .body("[2].progress", closeTo(10.0, 0.01)); + + // Verify allowances for user 2 + given() + .when() + .get("/user/2/allowance") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(101.0, 0.01)); + + // Verify history for both users + for (int userId = 1; userId <= 2; userId++) { + given() + .when() + .get("/user/" + userId + "/history") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].allowance", closeTo(101.0, 0.01)); + } + } + + @Test + void completeTaskWithNoWeights() { + int taskId = createTestTask(101); + + given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); + + // Ensure main allowance has no weight + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 0, "weight", 0, "colour", "")) + .when() + .put("/user/1/allowance/0") + .then() + .statusCode(200); + + // Complete the task + given().when().post("/task/" + taskId + "/complete").then().statusCode(200); + + // Verify task is completed + given().when().get("/task/" + taskId).then().statusCode(404); + + // Verify allowances for user 1 + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(101.0, 0.01)); + + // Verify allowances for user 2 + given() + .when() + .get("/user/2/allowance") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(101.0, 0.01)); + } + + @Test + void completeTaskAllowanceWeightsSumTo0() { + int taskId = createTestTask(101); + + given().when().get("/tasks").then().statusCode(200).body("$.size()", is(1)); + + // Update rest allowance to 0 weight + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 0, "weight", 0, "colour", "")) + .when() + .put("/user/1/allowance/0") + .then() + .statusCode(200); + + // Create allowance with 0 weight + createAllowance(1, "Test Allowance 1", 1000, 0); + + // Complete the task + given().when().post("/task/" + taskId + "/complete").then().statusCode(200); + + // Verify allowances for user 1 + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(101.0, 0.01)) + .body("[1].progress", closeTo(0.0, 0.01)); + } + + @Test + void completeTaskInvalidId() { + given().when().post("/task/999/complete").then().statusCode(404); + } + + // ---- Complete Allowance Tests ---- + + @Test + void completeAllowance() { + createTestTask(100); + createAllowance(1, "Test Allowance 1", 100, 50); + + // Update base allowance to 0 weight + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 0, "weight", 0, "colour", "")) + .when() + .put("/user/1/allowance/0") + .then() + .statusCode(200); + + // Complete the task + given().when().post("/task/1/complete").then().statusCode(200); + + // Get the allowance ID (first named allowance) + int allowanceId = given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .extract() + .path("[1].id"); + + // Complete allowance + given().when().post("/user/1/allowance/" + allowanceId + "/complete").then().statusCode(200); + + // Verify allowance no longer exists + given().when().get("/user/1/allowance/" + allowanceId).then().statusCode(404); + + // Verify history (newest first) + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].allowance", closeTo(-100.0, 0.01)) + .body("[0].description", is("Allowance completed: Test Allowance 1")) + .body("[1].allowance", closeTo(100.0, 0.01)) + .body("[1].description", is("Task completed: Test Task")); + } + + @Test + void completeAllowanceInvalidUserId() { + given().when().post("/user/999/allowance/1/complete").then().statusCode(404); + } + + @Test + void completeAllowanceInvalidAllowanceId() { + given().when().post("/user/1/allowance/999/complete").then().statusCode(404); + } + + // ---- Bulk Update Tests ---- + + @Test + void putBulkAllowance() { + int id1 = createAllowance(1, "Test Allowance 1", 1000, 1); + int id2 = createAllowance(1, "Test Allowance 2", 1000, 2); + + given() + .contentType(ContentType.JSON) + .body(List.of( + Map.of("id", id1, "weight", 5), + Map.of("id", 0, "weight", 99), + Map.of("id", id2, "weight", 10))) + .when() + .put("/user/1/allowance") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("$.size()", is(3)) + .body("[0].id", is(0)) + .body("[0].weight", closeTo(99.0, 0.01)) + .body("[1].id", is(id1)) + .body("[1].weight", closeTo(5.0, 0.01)) + .body("[2].id", is(id2)) + .body("[2].weight", closeTo(10.0, 0.01)); + } + + // ---- Add Allowance Amount Tests ---- + + @Test + void addAllowanceSimple() { + int allowanceId = createAllowance(1, "Test Allowance 1", 1000, 1); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/" + allowanceId + "/add") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[1].id", is(allowanceId)) + .body("[1].progress", closeTo(10.0, 0.01)); + + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].allowance", closeTo(10.0, 0.01)) + .body("[0].description", is("Added to allowance 1")); + } + + @Test + void addAllowanceWithSpillage() { + int id1 = createAllowance(1, "Test Allowance 1", 5, 1); + int id2 = createAllowance(1, "Test Allowance 2", 5, 1); + given() + .contentType(ContentType.JSON) + .body(Map.of("name", "", "target", 0, "weight", 1, "colour", "")) + .when() + .put("/user/1/allowance/0") + .then() + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/" + id1 + "/add") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[1].id", is(id1)) + .body("[1].progress", closeTo(5.0, 0.01)) + .body("[2].id", is(id2)) + .body("[2].progress", closeTo(2.5, 0.01)) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(2.5, 0.01)); + + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].allowance", closeTo(10.0, 0.01)) + .body("[0].description", is("Added to allowance 1")); + } + + @Test + void addAllowanceIdZero() { + createAllowance(1, "Test Allowance 1", 1000, 1); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/0/add") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(10.0, 0.01)); + + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(1)) + .body("[0].allowance", closeTo(10.0, 0.01)) + .body("[0].description", is("Added to allowance 1")); + } + + @Test + void subtractAllowanceSimple() { + int allowanceId = createAllowance(1, "Test Allowance 1", 1000, 1); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/" + allowanceId + "/add") + .then() + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", -2.5, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/" + allowanceId + "/add") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[1].id", is(allowanceId)) + .body("[1].progress", closeTo(7.5, 0.01)); + + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].allowance", closeTo(-2.5, 0.01)) + .body("[0].description", is("Added to allowance 1")) + .body("[1].allowance", closeTo(10.0, 0.01)) + .body("[1].description", is("Added to allowance 1")); + } + + @Test + void subtractAllowanceIdZero() { + createAllowance(1, "Test Allowance 1", 1000, 1); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/0/add") + .then() + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", -2.5, "description", "Added to allowance 1")) + .when() + .post("/user/1/allowance/0/add") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[0].id", is(0)) + .body("[0].progress", closeTo(7.5, 0.01)); + + given() + .when() + .get("/user/1/history") + .then() + .statusCode(200) + .body("$.size()", is(2)) + .body("[0].allowance", closeTo(-2.5, 0.01)) + .body("[0].description", is("Added to allowance 1")) + .body("[1].allowance", closeTo(10.0, 0.01)) + .body("[1].description", is("Added to allowance 1")); + } + + // ---- Transfer Tests ---- + + @Test + void transferSuccessful() { + int id1 = createAllowance(1, "From Allowance", 100, 1); + int id2 = createAllowance(1, "To Allowance", 100, 1); + + // Add 30 to allowance 1 + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 30, "description", "funds")) + .when() + .post("/user/1/allowance/" + id1 + "/add") + .then() + .statusCode(200); + + // Transfer 10 from 1 to 2 + given() + .contentType(ContentType.JSON) + .body(Map.of("from", id1, "to", id2, "amount", 10)) + .when() + .post("/transfer") + .then() + .statusCode(200) + .body("message", is("Transfer successful")); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[1].progress", closeTo(20.0, 0.01)) + .body("[2].progress", closeTo(10.0, 0.01)); + } + + @Test + void transferCapsAtTarget() { + int id1 = createAllowance(1, "From Allowance", 100, 1); + int id2 = createAllowance(1, "To Allowance", 5, 1); + + // Add 10 to allowance 1 + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "funds")) + .when() + .post("/user/1/allowance/" + id1 + "/add") + .then() + .statusCode(200); + + // Transfer 10, but to only needs 5 + given() + .contentType(ContentType.JSON) + .body(Map.of("from", id1, "to", id2, "amount", 10)) + .when() + .post("/transfer") + .then() + .statusCode(200); + + given() + .when() + .get("/user/1/allowance") + .then() + .statusCode(200) + .body("[1].progress", closeTo(5.0, 0.01)) + .body("[2].progress", closeTo(5.0, 0.01)); + } + + @Test + void transferDifferentUsersFails() { + int id1 = createAllowance(1, "User1 Allowance", 100, 1); + + // Create allowance for user 2 + int id2 = createAllowance(2, "User2 Allowance", 100, 1); + + // Add to user1 allowance + given() + .contentType(ContentType.JSON) + .body(Map.of("amount", 10, "description", "funds")) + .when() + .post("/user/1/allowance/" + id1 + "/add") + .then() + .statusCode(200); + + // Transfer between different users + given() + .contentType(ContentType.JSON) + .body(Map.of("from", id1, "to", id2, "amount", 5)) + .when() + .post("/transfer") + .then() + .statusCode(400); + } + + @Test + void transferInsufficientFunds() { + int id1 = createAllowance(1, "From Allowance", 100, 1); + int id2 = createAllowance(1, "To Allowance", 100, 1); + + given() + .contentType(ContentType.JSON) + .body(Map.of("from", id1, "to", id2, "amount", 10)) + .when() + .post("/transfer") + .then() + .statusCode(400) + .body("error", containsStringIgnoringCase("insufficient")); + } + + @Test + void transferNotFound() { + given() + .contentType(ContentType.JSON) + .body(Map.of("from", 999, "to", 1000, "amount", 1)) + .when() + .post("/transfer") + .then() + .statusCode(404); + } + + // ---- Helpers ---- + + private int createTestTask(int reward) { + return given() + .contentType(ContentType.JSON) + .body(Map.of("name", "Test Task", "reward", reward)) + .when() + .post("/tasks") + .then() + .statusCode(201) + .extract() + .path("id"); + } + + private int createAllowance(int userId, String name, double target, double weight) { + return given() + .contentType(ContentType.JSON) + .body(Map.of("name", name, "target", target, "weight", weight)) + .when() + .post("/user/" + userId + "/allowance") + .then() + .statusCode(201) + .extract() + .path("id"); + } + + private int createAllowanceWithColour(int userId, String name, double target, double weight, String colour) { + return given() + .contentType(ContentType.JSON) + .body(Map.of("name", name, "target", target, "weight", weight, "colour", colour)) + .when() + .post("/user/" + userId + "/allowance") + .then() + .statusCode(201) + .extract() + .path("id"); + } } diff --git a/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ColourUtilTest.java b/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ColourUtilTest.java index 2e7766a..f2252ff 100644 --- a/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ColourUtilTest.java +++ b/backend-spring/src/test/java/be/seeseepuff/allowanceplanner/ColourUtilTest.java @@ -5,29 +5,24 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -class ColourUtilTest -{ - @Test - void convertStringToColourWithSign() - { - assertEquals(0x123456, ColourUtil.convertStringToColour("#123456")); - } +class ColourUtilTest { + @Test + void convertStringToColourWithSign() { + assertEquals(0x123456, ColourUtil.convertStringToColour("#123456")); + } - @Test - void convertStringToColourWithoutSign() - { - assertEquals(0x123456, ColourUtil.convertStringToColour("123456")); - } + @Test + void convertStringToColourWithoutSign() { + assertEquals(0x123456, ColourUtil.convertStringToColour("123456")); + } - @Test - void convertStringToColourWithSignThreeDigits() - { - assertEquals(0xA0B0C0, ColourUtil.convertStringToColour("#ABC")); - } + @Test + void convertStringToColourWithSignThreeDigits() { + assertEquals(0xA0B0C0, ColourUtil.convertStringToColour("#ABC")); + } - @Test - void convertStringToColourWithoutSignThreeDigits() - { - assertEquals(0xA0B0C0, ColourUtil.convertStringToColour("ABC")); - } + @Test + void convertStringToColourWithoutSignThreeDigits() { + assertEquals(0xA0B0C0, ColourUtil.convertStringToColour("ABC")); + } } -- 2.49.1 From b3410e3a5f958b49638dd7897be1ff976b54fac6 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Mon, 2 Mar 2026 18:08:47 +0100 Subject: [PATCH 4/9] Add Eclipse files to gitignore --- backend-spring/.gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend-spring/.gitignore b/backend-spring/.gitignore index 67bcc2f..b7d2629 100644 --- a/backend-spring/.gitignore +++ b/backend-spring/.gitignore @@ -1,2 +1,7 @@ .gradle/ build/ + +# Eclispe Directories +/.classpath +/.project +/bin -- 2.49.1 From 94380db02dc99ed2621857eb4f37dd549f984b1a Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Mon, 2 Mar 2026 18:09:08 +0100 Subject: [PATCH 5/9] Use lombok @getter @setter --- .../allowanceplanner/entity/Allowance.java | 60 ++----------------- .../allowanceplanner/entity/History.java | 44 ++------------ .../allowanceplanner/entity/Task.java | 60 ++----------------- .../allowanceplanner/entity/User.java | 36 ++--------- 4 files changed, 16 insertions(+), 184 deletions(-) diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java index 68e02cb..2db2881 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Allowance.java @@ -1,9 +1,13 @@ package be.seeseepuff.allowanceplanner.entity; import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; @Entity @Table(name = "allowances") +@Getter +@Setter public class Allowance { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -25,60 +29,4 @@ public class Allowance { private double weight; private Integer colour; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public int getUserId() { - return userId; - } - - public void setUserId(int userId) { - this.userId = userId; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public long getTarget() { - return target; - } - - public void setTarget(long target) { - this.target = target; - } - - public long getBalance() { - return balance; - } - - public void setBalance(long balance) { - this.balance = balance; - } - - public double getWeight() { - return weight; - } - - public void setWeight(double weight) { - this.weight = weight; - } - - public Integer getColour() { - return colour; - } - - public void setColour(Integer colour) { - this.colour = colour; - } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java index 6f7340e..c038d03 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/History.java @@ -1,9 +1,13 @@ package be.seeseepuff.allowanceplanner.entity; import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; @Entity @Table(name = "history") +@Getter +@Setter public class History { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -19,44 +23,4 @@ public class History { private long amount; private String description; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public int getUserId() { - return userId; - } - - public void setUserId(int userId) { - this.userId = userId; - } - - public long getTimestamp() { - return timestamp; - } - - public void setTimestamp(long timestamp) { - this.timestamp = timestamp; - } - - public long getAmount() { - return amount; - } - - public void setAmount(long amount) { - this.amount = amount; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java index da662e1..c4451ab 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/Task.java @@ -1,9 +1,13 @@ package be.seeseepuff.allowanceplanner.entity; import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; @Entity @Table(name = "tasks") +@Getter +@Setter public class Task { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -23,60 +27,4 @@ public class Task { @Column(name = "next_run") private Long nextRun; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public long getReward() { - return reward; - } - - public void setReward(long reward) { - this.reward = reward; - } - - public Integer getAssigned() { - return assigned; - } - - public void setAssigned(Integer assigned) { - this.assigned = assigned; - } - - public String getSchedule() { - return schedule; - } - - public void setSchedule(String schedule) { - this.schedule = schedule; - } - - public Long getCompleted() { - return completed; - } - - public void setCompleted(Long completed) { - this.completed = completed; - } - - public Long getNextRun() { - return nextRun; - } - - public void setNextRun(Long nextRun) { - this.nextRun = nextRun; - } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java index 8a74483..e55f46b 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/entity/User.java @@ -1,9 +1,13 @@ package be.seeseepuff.allowanceplanner.entity; import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; @Entity @Table(name = "users") +@Getter +@Setter public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -17,36 +21,4 @@ public class User { @Column(nullable = false) private long balance = 0; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public double getWeight() { - return weight; - } - - public void setWeight(double weight) { - this.weight = weight; - } - - public long getBalance() { - return balance; - } - - public void setBalance(long balance) { - this.balance = balance; - } } -- 2.49.1 From 6beba890e853501b4516fe9f0bed501a5c6ac479 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Mon, 2 Mar 2026 18:09:26 +0100 Subject: [PATCH 6/9] Ignore more eclipse stuff --- backend-spring/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/backend-spring/.gitignore b/backend-spring/.gitignore index b7d2629..e87a22f 100644 --- a/backend-spring/.gitignore +++ b/backend-spring/.gitignore @@ -5,3 +5,4 @@ build/ /.classpath /.project /bin +/.settings -- 2.49.1 From 03aa050f6a0b7f3ddd6254844dbc24aaf9281c16 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Mon, 2 Mar 2026 18:09:38 +0100 Subject: [PATCH 7/9] Actually add lombok support --- backend-spring/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend-spring/build.gradle.kts b/backend-spring/build.gradle.kts index 069add2..cdfd23a 100644 --- a/backend-spring/build.gradle.kts +++ b/backend-spring/build.gradle.kts @@ -24,6 +24,8 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-flyway") implementation("org.flywaydb:flyway-database-postgresql") runtimeOnly("org.postgresql:postgresql") + compileOnly("org.projectlombok:lombok") + annotationProcessor("org.projectlombok:lombok") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-testcontainers") -- 2.49.1 From e2ac1bfd3da70db6747c468e718dc84458abbf22 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Mon, 2 Mar 2026 18:10:01 +0100 Subject: [PATCH 8/9] Split API controller --- ...ntroller.java => AllowanceController.java} | 192 +----------------- .../controller/HistoryController.java | 57 ++++++ .../controller/MigrationController.java | 23 +++ .../controller/TaskController.java | 116 +++++++++++ .../controller/TransferController.java | 28 +++ .../controller/UserController.java | 42 ++++ 6 files changed, 270 insertions(+), 188 deletions(-) rename backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/{ApiController.java => AllowanceController.java} (55%) create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/HistoryController.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/MigrationController.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/TaskController.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/TransferController.java create mode 100644 backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/UserController.java diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/AllowanceController.java similarity index 55% rename from backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java rename to backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/AllowanceController.java index 3c8d971..bf39dd2 100644 --- a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/ApiController.java +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/AllowanceController.java @@ -1,7 +1,8 @@ package be.seeseepuff.allowanceplanner.controller; import be.seeseepuff.allowanceplanner.dto.*; -import be.seeseepuff.allowanceplanner.service.*; +import be.seeseepuff.allowanceplanner.service.AllowanceService; +import be.seeseepuff.allowanceplanner.service.UserService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -12,86 +13,15 @@ import java.util.Optional; @RestController @RequestMapping("/api") @CrossOrigin(origins = "*") -public class ApiController { +public class AllowanceController { private final UserService userService; private final AllowanceService allowanceService; - private final TaskService taskService; - private final TransferService transferService; - private final MigrationService migrationService; - public ApiController(UserService userService, - AllowanceService allowanceService, - TaskService taskService, - TransferService transferService, - MigrationService migrationService) { + public AllowanceController(UserService userService, AllowanceService allowanceService) { this.userService = userService; this.allowanceService = allowanceService; - this.taskService = taskService; - this.transferService = transferService; - this.migrationService = migrationService; } - // ---- Users ---- - - @GetMapping("/users") - public List getUsers() { - return userService.getUsers(); - } - - @GetMapping("/user/{userId}") - public ResponseEntity getUser(@PathVariable String userId) { - int id; - try { - id = Integer.parseInt(userId); - } catch (NumberFormatException e) { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } - - Optional user = userService.getUser(id); - if (user.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } - return ResponseEntity.ok(user.get()); - } - - // ---- History ---- - - @PostMapping("/user/{userId}/history") - public ResponseEntity postHistory(@PathVariable String userId, @RequestBody PostHistoryRequest request) { - int id; - try { - id = Integer.parseInt(userId); - } catch (NumberFormatException e) { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } - - if (request.description() == null || request.description().isEmpty()) { - return ResponseEntity.badRequest().body(new ErrorResponse("Description cannot be empty")); - } - - if (!userService.userExists(id)) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } - - allowanceService.addHistory(id, request); - return ResponseEntity.ok(new MessageResponse("History updated successfully")); - } - - @GetMapping("/user/{userId}/history") - public ResponseEntity getHistory(@PathVariable String userId) { - int id; - try { - id = Integer.parseInt(userId); - } catch (NumberFormatException e) { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); - } - - List history = allowanceService.getHistory(id); - return ResponseEntity.ok(history); - } - - // ---- Allowances ---- - @GetMapping("/user/{userId}/allowance") public ResponseEntity getUserAllowance(@PathVariable String userId) { int id; @@ -288,118 +218,4 @@ public class ApiController { } return ResponseEntity.ok(new MessageResponse("Allowance completed successfully")); } - - // ---- Tasks ---- - - @PostMapping("/tasks") - public ResponseEntity createTask(@RequestBody CreateTaskRequest request) { - if (request.name() == null || request.name().isEmpty()) { - return ResponseEntity.badRequest().body(new ErrorResponse("Task name cannot be empty")); - } - - if (request.schedule() != null) { - return ResponseEntity.badRequest().body(new ErrorResponse("Schedules are not yet supported")); - } - - if (request.assigned() != null) { - if (!userService.userExists(request.assigned())) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); - } - } - - int taskId = taskService.createTask(request); - return ResponseEntity.status(HttpStatus.CREATED).body(new IdResponse(taskId)); - } - - @GetMapping("/tasks") - public List getTasks() { - return taskService.getTasks(); - } - - @GetMapping("/task/{taskId}") - public ResponseEntity getTask(@PathVariable String taskId) { - int id; - try { - id = Integer.parseInt(taskId); - } catch (NumberFormatException e) { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); - } - - Optional task = taskService.getTask(id); - if (task.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); - } - return ResponseEntity.ok(task.get()); - } - - @PutMapping("/task/{taskId}") - public ResponseEntity putTask(@PathVariable String taskId, @RequestBody CreateTaskRequest request) { - int id; - try { - id = Integer.parseInt(taskId); - } catch (NumberFormatException e) { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); - } - - Optional existing = taskService.getTask(id); - if (existing.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); - } - - taskService.updateTask(id, request); - return ResponseEntity.ok(new MessageResponse("Task updated successfully")); - } - - @DeleteMapping("/task/{taskId}") - public ResponseEntity deleteTask(@PathVariable String taskId) { - int id; - try { - id = Integer.parseInt(taskId); - } catch (NumberFormatException e) { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); - } - - if (!taskService.hasTask(id)) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); - } - - taskService.deleteTask(id); - return ResponseEntity.ok(new MessageResponse("Task deleted successfully")); - } - - @PostMapping("/task/{taskId}/complete") - public ResponseEntity completeTask(@PathVariable String taskId) { - int id; - try { - id = Integer.parseInt(taskId); - } catch (NumberFormatException e) { - return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); - } - - boolean completed = taskService.completeTask(id); - if (!completed) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); - } - return ResponseEntity.ok(new MessageResponse("Task completed successfully")); - } - - // ---- Transfer ---- - - @PostMapping("/transfer") - public ResponseEntity transfer(@RequestBody TransferRequest request) { - TransferService.TransferResult result = transferService.transfer(request); - return switch (result.status()) { - case SUCCESS -> ResponseEntity.ok(new MessageResponse(result.message())); - case BAD_REQUEST -> ResponseEntity.badRequest().body(new ErrorResponse(result.message())); - case NOT_FOUND -> ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(result.message())); - }; - } - - // ---- Migration ---- - - @PostMapping("/import") - public ResponseEntity importData(@RequestBody MigrationDto data) { - migrationService.importData(data); - return ResponseEntity.ok(new MessageResponse("Import successful")); - } } diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/HistoryController.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/HistoryController.java new file mode 100644 index 0000000..4da46ea --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/HistoryController.java @@ -0,0 +1,57 @@ +package be.seeseepuff.allowanceplanner.controller; + +import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.service.AllowanceService; +import be.seeseepuff.allowanceplanner.service.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api") +@CrossOrigin(origins = "*") +public class HistoryController { + private final UserService userService; + private final AllowanceService allowanceService; + + public HistoryController(UserService userService, AllowanceService allowanceService) { + this.userService = userService; + this.allowanceService = allowanceService; + } + + @PostMapping("/user/{userId}/history") + public ResponseEntity postHistory(@PathVariable String userId, @RequestBody PostHistoryRequest request) { + int id; + try { + id = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + if (request.description() == null || request.description().isEmpty()) { + return ResponseEntity.badRequest().body(new ErrorResponse("Description cannot be empty")); + } + + if (!userService.userExists(id)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + + allowanceService.addHistory(id, request); + return ResponseEntity.ok(new MessageResponse("History updated successfully")); + } + + @GetMapping("/user/{userId}/history") + public ResponseEntity getHistory(@PathVariable String userId) { + int id; + try { + id = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + List history = allowanceService.getHistory(id); + return ResponseEntity.ok(history); + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/MigrationController.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/MigrationController.java new file mode 100644 index 0000000..d0228f1 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/MigrationController.java @@ -0,0 +1,23 @@ +package be.seeseepuff.allowanceplanner.controller; + +import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.service.MigrationService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api") +@CrossOrigin(origins = "*") +public class MigrationController { + private final MigrationService migrationService; + + public MigrationController(MigrationService migrationService) { + this.migrationService = migrationService; + } + + @PostMapping("/import") + public ResponseEntity importData(@RequestBody MigrationDto data) { + migrationService.importData(data); + return ResponseEntity.ok(new MessageResponse("Import successful")); + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/TaskController.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/TaskController.java new file mode 100644 index 0000000..b3ec889 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/TaskController.java @@ -0,0 +1,116 @@ +package be.seeseepuff.allowanceplanner.controller; + +import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.service.TaskService; +import be.seeseepuff.allowanceplanner.service.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api") +@CrossOrigin(origins = "*") +public class TaskController { + private final UserService userService; + private final TaskService taskService; + + public TaskController(UserService userService, TaskService taskService) { + this.userService = userService; + this.taskService = taskService; + } + + @PostMapping("/tasks") + public ResponseEntity createTask(@RequestBody CreateTaskRequest request) { + if (request.name() == null || request.name().isEmpty()) { + return ResponseEntity.badRequest().body(new ErrorResponse("Task name cannot be empty")); + } + + if (request.schedule() != null) { + return ResponseEntity.badRequest().body(new ErrorResponse("Schedules are not yet supported")); + } + + if (request.assigned() != null) { + if (!userService.userExists(request.assigned())) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + } + + int taskId = taskService.createTask(request); + return ResponseEntity.status(HttpStatus.CREATED).body(new IdResponse(taskId)); + } + + @GetMapping("/tasks") + public List getTasks() { + return taskService.getTasks(); + } + + @GetMapping("/task/{taskId}") + public ResponseEntity getTask(@PathVariable String taskId) { + int id; + try { + id = Integer.parseInt(taskId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } + + Optional task = taskService.getTask(id); + if (task.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } + return ResponseEntity.ok(task.get()); + } + + @PutMapping("/task/{taskId}") + public ResponseEntity putTask(@PathVariable String taskId, @RequestBody CreateTaskRequest request) { + int id; + try { + id = Integer.parseInt(taskId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } + + Optional existing = taskService.getTask(id); + if (existing.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } + + taskService.updateTask(id, request); + return ResponseEntity.ok(new MessageResponse("Task updated successfully")); + } + + @DeleteMapping("/task/{taskId}") + public ResponseEntity deleteTask(@PathVariable String taskId) { + int id; + try { + id = Integer.parseInt(taskId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } + + if (!taskService.hasTask(id)) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } + + taskService.deleteTask(id); + return ResponseEntity.ok(new MessageResponse("Task deleted successfully")); + } + + @PostMapping("/task/{taskId}/complete") + public ResponseEntity completeTask(@PathVariable String taskId) { + int id; + try { + id = Integer.parseInt(taskId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid task ID")); + } + + boolean completed = taskService.completeTask(id); + if (!completed) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Task not found")); + } + return ResponseEntity.ok(new MessageResponse("Task completed successfully")); + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/TransferController.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/TransferController.java new file mode 100644 index 0000000..b251215 --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/TransferController.java @@ -0,0 +1,28 @@ +package be.seeseepuff.allowanceplanner.controller; + +import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.service.TransferService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api") +@CrossOrigin(origins = "*") +public class TransferController { + private final TransferService transferService; + + public TransferController(TransferService transferService) { + this.transferService = transferService; + } + + @PostMapping("/transfer") + public ResponseEntity transfer(@RequestBody TransferRequest request) { + TransferService.TransferResult result = transferService.transfer(request); + return switch (result.status()) { + case SUCCESS -> ResponseEntity.ok(new MessageResponse(result.message())); + case BAD_REQUEST -> ResponseEntity.badRequest().body(new ErrorResponse(result.message())); + case NOT_FOUND -> ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(result.message())); + }; + } +} diff --git a/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/UserController.java b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/UserController.java new file mode 100644 index 0000000..ec01a9a --- /dev/null +++ b/backend-spring/src/main/java/be/seeseepuff/allowanceplanner/controller/UserController.java @@ -0,0 +1,42 @@ +package be.seeseepuff.allowanceplanner.controller; + +import be.seeseepuff.allowanceplanner.dto.*; +import be.seeseepuff.allowanceplanner.service.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api") +@CrossOrigin(origins = "*") +public class UserController { + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/users") + public List getUsers() { + return userService.getUsers(); + } + + @GetMapping("/user/{userId}") + public ResponseEntity getUser(@PathVariable String userId) { + int id; + try { + id = Integer.parseInt(userId); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(new ErrorResponse("Invalid user ID")); + } + + Optional user = userService.getUser(id); + if (user.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + return ResponseEntity.ok(user.get()); + } +} -- 2.49.1 From 548342798e8404e33b50a6769e46981e599837f6 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Mon, 2 Mar 2026 18:12:10 +0100 Subject: [PATCH 9/9] Use Lombok plugin --- backend-spring/build.gradle.kts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend-spring/build.gradle.kts b/backend-spring/build.gradle.kts index cdfd23a..48d74f6 100644 --- a/backend-spring/build.gradle.kts +++ b/backend-spring/build.gradle.kts @@ -2,6 +2,7 @@ plugins { java id("org.springframework.boot") version "4.0.3" id("io.spring.dependency-management") version "1.1.7" + id("io.freefair.lombok") version "9.2.0" } group = "be.seeseepuff" @@ -24,8 +25,6 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-flyway") implementation("org.flywaydb:flyway-database-postgresql") runtimeOnly("org.postgresql:postgresql") - compileOnly("org.projectlombok:lombok") - annotationProcessor("org.projectlombok:lombok") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-testcontainers") -- 2.49.1