From 51903f2d3775079b95a28b860bd1299ac8a2a6f2 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sat, 18 Apr 2026 20:36:46 -0600 Subject: [PATCH 01/34] fix closed conversation messages --- web/app/ai-chat/page.js | 2 ++ web/app/chat/page.js | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/web/app/ai-chat/page.js b/web/app/ai-chat/page.js index 13ed9ae4..64f3ae80 100644 --- a/web/app/ai-chat/page.js +++ b/web/app/ai-chat/page.js @@ -440,6 +440,8 @@ function AiChatPage() { setMessages([]); setError(null); setBotTyping(false); + await Promise.all([fetchConversation(convId), fetchMessages(convId)]); + connectStomp(convId); router.replace(`/ai-chat?id=${convId}`, { scroll: false }); } diff --git a/web/app/chat/page.js b/web/app/chat/page.js index 17d7c42c..a52eed4d 100644 --- a/web/app/chat/page.js +++ b/web/app/chat/page.js @@ -422,6 +422,10 @@ function ChatPage() { if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } setMessages([]); setError(null); + setSwitchingConv(true); + await Promise.all([fetchConversation(convId), fetchMessages(convId)]); + setSwitchingConv(false); + connectStomp(convId); router.replace(`/chat?id=${convId}`, { scroll: false }); } -- 2.49.1 From fe0b00bcd1503858b86a1a068841e9136c8a46f3 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Sun, 19 Apr 2026 08:25:14 -0600 Subject: [PATCH 02/34] Mobile UI for ai chat, fixed backend issue --- .../repository/AppointmentRepository.java | 2 +- web/app/ai-chat/page.js | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index 59243df2..b23b1902 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -53,7 +53,7 @@ public interface AppointmentRepository extends JpaRepository List findByPet_Id(Long petId); - @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.pet.petId = :petId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.pet.id = :petId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByPetIdAndAppointmentDate(@Param("petId") Long petId, @Param("date") LocalDate date); List findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status); diff --git a/web/app/ai-chat/page.js b/web/app/ai-chat/page.js index 13ed9ae4..b6ac0816 100644 --- a/web/app/ai-chat/page.js +++ b/web/app/ai-chat/page.js @@ -68,11 +68,23 @@ function AttachmentPreview({ url, name, token }) { ); } +function useIsMobile() { + const [isMobile, setIsMobile] = useState(false); + useEffect(() => { + const check = () => setIsMobile(window.innerWidth < 640); + check(); + window.addEventListener("resize", check); + return () => window.removeEventListener("resize", check); + }, []); + return isMobile; +} + function AiChatPage() { const { user, token, loading: authLoading } = useAuth(); const router = useRouter(); const searchParams = useSearchParams(); const conversationIdParam = searchParams.get("id"); + const isMobile = useIsMobile(); const [conversation, setConversation] = useState(null); const [messages, setMessages] = useState([]); @@ -496,8 +508,8 @@ function AiChatPage() {
-
-
+
+
All Conversations
@@ -552,7 +564,7 @@ function AiChatPage() {
-
+
{!conversation ? (
🐾
@@ -568,7 +580,7 @@ function AiChatPage() {
) : (
-
+
{isEscalated ? "👤" : "🐾"}
@@ -581,14 +593,14 @@ function AiChatPage() {
-
+
{!isEscalated && !isClosed && ( - )} {!isClosed && ( - )} @@ -790,7 +802,6 @@ const s = { padding: "1.5rem 1rem 2rem", }, sidebar: { - width: 230, flexShrink: 0, background: "white", borderRadius: 16, @@ -798,8 +809,7 @@ const s = { display: "flex", flexDirection: "column", overflow: "hidden", - maxHeight: "calc(100vh - 220px)", - minHeight: 300, + minHeight: 200, }, sidebarHeader: { display: "flex", @@ -899,7 +909,7 @@ const s = { display: "flex", flexDirection: "column", height: "calc(100vh - 220px)", - minHeight: 450, + minHeight: 400, }, chatHeader: { display: "flex", -- 2.49.1 From 33bb0a3ad79cd1b4a538f786d27f6ba4ba417e45 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Sun, 19 Apr 2026 10:33:17 -0600 Subject: [PATCH 03/34] Plethora of changes --- web/app/ai-chat/page.js | 12 ++++++------ web/app/page.js | 6 +++--- web/app/profile/page.js | 4 ++-- web/components/Navigation.js | 2 +- web/components/ProductProfile.js | 12 +++++++----- web/public/images/pet-placeholder.png | Bin 105870 -> 0 bytes web/public/logo.png | Bin 0 -> 113586 bytes web/public/logo_simple.png | Bin 105870 -> 0 bytes 8 files changed, 19 insertions(+), 17 deletions(-) delete mode 100644 web/public/images/pet-placeholder.png create mode 100644 web/public/logo.png delete mode 100644 web/public/logo_simple.png diff --git a/web/app/ai-chat/page.js b/web/app/ai-chat/page.js index b6ac0816..883e6094 100644 --- a/web/app/ai-chat/page.js +++ b/web/app/ai-chat/page.js @@ -595,12 +595,12 @@ function AiChatPage() {
{!isEscalated && !isClosed && ( - )} {!isClosed && ( - )} @@ -961,8 +961,8 @@ const s = { border: "2px solid #ff8c00", color: "#ff8c00", borderRadius: 8, - padding: "0.45rem 0.9rem", - fontSize: "0.82rem", + padding: "0.3rem 0.65rem", + fontSize: "0.72rem", fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap", @@ -1005,8 +1005,8 @@ const s = { border: "2px solid #c0392b", color: "#c0392b", borderRadius: 8, - padding: "0.45rem 0.9rem", - fontSize: "0.82rem", + padding: "0.3rem 0.65rem", + fontSize: "0.72rem", fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap", diff --git a/web/app/page.js b/web/app/page.js index 7ec75afe..a9394ecf 100644 --- a/web/app/page.js +++ b/web/app/page.js @@ -66,11 +66,11 @@ export default function Home() {
-

What We Do

+

What We Do

Leon's Pet Store is a full-service pet shop offering adoptions, grooming, veterinary appointments, and a wide range of supplies to keep your pets happy and healthy.

-

Our Focus

+

Our Focus

  • Support responsible pet adoption
  • Provide grooming and care services
  • @@ -79,7 +79,7 @@ export default function Home() {
-

Visit the Store

+

Visit the Store

Come visit us in person or explore our services online. Whether you're a first-time pet owner or a seasoned animal lover, we're here to help every step of the way.

diff --git a/web/app/profile/page.js b/web/app/profile/page.js index d76487c1..39cc17dc 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -444,7 +444,7 @@ export default function ProfilePage() { {fields.map(({ label, value }) => (
{label}
-
{value}
+
{value}
))} @@ -552,7 +552,7 @@ export default function ProfilePage() {
- - {feedback && ( -

- {feedback.message} -

- )} +
+ {feedback && ( +

+ {feedback.message} +

+ )} +
)}
diff --git a/web/public/images/pet-placeholder.png b/web/public/images/pet-placeholder.png deleted file mode 100644 index 207e9d297102df82805bf41e157934a33ac4bf34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105870 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

NAFM|qBWq5P9YQ_X2=34EjT149+ERt1$-+j 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 Eg3hk)xc~qF diff --git a/web/public/logo.png b/web/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..58d6808fc5a138a4f98dd24bbf7f51cdf04803c5 GIT binary patch literal 113586 zcmX6^b99{D*KN|+Nn_h=W81db*qxY-Z8dgd+qP{t#>7tMoA>uUYdz~(YyP@(&%XPd zv(LT}%8F9R2>1wJzI;KJkrr3|@&)4lzZVV${L94hP9*pP?_my9gj zFAyC+WyD3)J+dx7+_NaPJb`gNxd25C*Qem@sd-u5s$VFil;7bP14m_yg^hcgqag?g ztARsL+qpG8JKpyGH?IAh&E`q=c4%ub5ag8*`N3bHDU%muGc9Rc-9x_l`DdNwq;Fx# z%uk1U0c{N%ek?4gm9$sn+!f7GkU`=9|AS(;s7fL%_P=LM*&r*YmmOoB#NWG>&;1J7Y3{N> z4yxTRMm0Tcb?odHX81wdv)7|4Pktw2Wh8X#cUcD%&S;&ZG+F&%u@gP+M^n91p zJmF4q+MW^1EVN`P=&lu@UfDI2IFuPuV1Wy^qc56y=fk=!8F!8SyzFbl*!aAbuTdL$ z-Z5|0vI`3f&BSiA`RS-jP5qNjw%wZ3k+u70zuNQ;Z?mlyvlP)`>veymr`gWz>^1Hz z?tN%z?qu(VVs+2Gk}lWQwQxrxpJXb5(BG2Cn<>;Q{`Hev!rTr(isge94N(g~f^V)p z8!wva7o>C|E>v7o>X%Z+d`u-QevdfSNO6MBCy#ShTK-gVfdhW7>{mhSHMHvblmh&PY_vOd_xf!zedkCv`#1}Hof#5AK z)4>r}xUaUAp*`P}Jt)0ErN&qyjr0|0ZcrvAs5X|fuYL3;9ZZ`sh1n@w`p=OooBscv zJxFNVP6~!-{yTp(^Wd!)%VI!UsgY*sx)%uJ+cBuh5?sGpZOT7f8tWqBi#6on{3Xuz zqhp!6&NiB?))lA<_W|HNYzlwkI`bnK{&OXg!r;|X9UyRvB9;SZn6|nbc zW3`FB)(ahKzuSjEd|CFPjb*U?2hHf`AP)XQ2m$O3!lj1&qQ6@ZanUD(iAZqXw)|-S zTj0f1=r9m^v2cYuf%#(pSGu)uN=BpTr~Phz9Q;n$G66UL#4+Z5bm3598my}y+U zpCTf+DTpi{+X~rB6(yXVrwT8IDCPNk@&CaDmhW}kA9OIyLGnIbmwuU>{Cd`C`s*k; z!Oe^O$4h%EX5qfivO|l834=JDi|NVJ+s{k%FM)v7cHt@7m~c|!PN+&^%;KrATzH$B zVgAuT45tX{8K@{IrT8wqO{?9m$lRdyE!H463ske$1&nx!z0X$kGUV4?^nqFa-%=rc zJ0y1Ulf>u|eKXMEI^nG*@U260FgJ}!(S|iriIg{WvT8bVTr5^i8&Fj@WWmObxlFfy z9ZMP5W>*!RuL(b29de^TYsde6F66lEZ5`+OJ`x`1I{J4v%*{IOE`X60gx>ZfMl$Zu zbvv%%zU6m*AeHSM!SJf2U+424?Bk%7@v}^uUt3NNud>Mlh>o5tbJou)kuVb9UM{w; zi>XV~S}*#$F~!(EK+8efu9-Cr$#v8#V2I#dNtE5?&htV5`K8wk6{81et`vk4SD7)% z!14a^Qg}}*1QNi4fmx?OMDjulAt*u$JrC^rG405XP5jiquvca$l87G|2kj}7K~7%B ziZ-j8l!jHnF^v1y($dOx{R^&nU|0^~Ykp^WH-RLdk+$bxXpO$hoSp@kvi8xlQbR+( zCJyIu5kA{(S~0)Pr%5{WT&^c+8c#oV9Ock8d>m*tODW1bnf(*6kM7?qHgaLRggR%C zU+z!KDKEz*ASk3o7@tq1geQYq*NLjHlwL?igPdL_wzT&_i!)TlI6A-n&PrlY9kbQ@ zE~`zLgW`>BIU_WZ7jEx}Bzhwv@D6yVd^cRoH6zXA+<#CL(vRI_xt@hR*KJ)6c}mM~ z=_Y;H)t9KqR!udJc9OQ6l55QWg{`M~G8k_`SKRdQm*KBaBdz@J0$LSncxIVRmjob! z%rOccUROTND@HjG5RA|m5l3z`bmkuZkE0Z=$~CuRB1>0y2ce4c zTLAm7c7~|J?^O+&^B8*AKV@Xy-7?q}Y{iJg#wFIIO7O^GijmB9=4F2bR$OnJCI4lV z6CWbBKNjFztTL~%+N~%T4RfjaioWl9h=<)RB+rzVMk1tWvK@bQ=^WQ7DF1j%*dc}Y z_*_xH+6!up2VH*_U_XNpIcX(LGjjm3j4}|k6Bvc;<0q>}$NRRL>ye5j5{4<55+du< zr3g|LOOZ3SUn9PAV@_k}Ct^z+a%x3vUNk(qlRQP!7z%CFIOw@fj-h4)#;AA{0t^O3 zB+X_CP%!^(#}G7Z^n6?n)PQDKa&y&(pc$e&^RkZ*%IQd?=okc1G$R5eqp&>RYyh@8oOJloaZr29>Q-Arjvfe1^9YIup$fK*a-|kQaG_?6m8l>*I zcDxot8#K$a@qs0w>`(#Eq5ti1-atVOS!gTt^)kAsFa{hXmu1*yqE|71ce75n@*waJc7~YVf*SSQ!F65GO^e z!0+UK^x^dR%r;i&W6;)8b4Ji<&++||Fy?sM>Gyxh?;|dTU~1=m$f#1~<+0?X{<$~- zEvGzC?`i@=thZ>c15hj_{UyRMFF1_t7`HKY`;;QToW5}x*FX!1%I}K0%#%FleV>OK{WN=E zq0G<4dWN|21N3L;*v$jRbk(2Kuz?i+%h#apJS?YtlQiheFz6bykCpT0PGupWF@Tdh zj`$E+^!k-Ps%7T~xs# zJu*V+Wl`jy#-XC5<-zY4)Zrs^UOZ+^dhbRtbe|vS8Ka$BW`nl!O&-_=8SgT`9uuZC z(W2`n77$n4I9$6EAK|;xla@u6LHBBL%1+}4vDMKs(dwv6^vr6bV(l$nFAj}dXcD$O z8@mCW8#;wT56|(&O(j;@5O@#hyFXToJ(=53*KRyb%4BcLIcv~%LC+?(@ev08& zE<>Sf4X4r_!wuFpm>K4YH%4xs7bsuvGqNz2!qmjfTy@PZwiypO7;+;xPDi-CF8IqpWv)15?oqf+8iE+NmU#W5Xq23V|bG)>OeQX&N z>^ao*l6!G@J`~6pB{u3EhYtlIf*jNRP8ro&uGaf# zJJx@`sL<;C3BR!lVWZw`Z4HYv!WDPc+%b{!bc@B$g6pV+vbJ7r_I&Y8w z!;FcE-bxPflW4)-W7OX)Ca)Z{S*uX8!*%U2v|PLE=W!kJ5qQg#s00KRHePi3=eGUc zAXDx%zGS#^?xz1^4>CXvYqZn^3Mwr!0-mkqJ}kKKP&yG{J_z{etvDa|0&?;Viq)tQ~{2n&{gF~5G^Q^E=MFxyI5@8~0*q^pTD-Aln1{X-H` z4oyYhY?2@FTWc=7h=NF#1rY#!4e>jSNc4i5N)K z9f{E}J%SnfgpxG`E)H#P+pN777K*Gy%}W{SFZG4;+EQTDY%}L4yKCuz%y+l$X|7(^@HCNYQvzr;e%VM0U zT`p)Mo<7H}LE8sq*C=~$)*K_h%YzR6hOB+&)j-0 z#FSzoF0mz`*SZS~FFee#(;Nd1 zHtxfoBjcat(EhPmc#hABYb39M!U(?|J;U~D0iL%` z(~*Bv={Gd^Cnv@Vi|#74HM54Su6`SIQEDBY108ivUTeJ!`X}ftC^zL2T<_)eKQEyq zNLkt&b)64>KNV3^z|(y=Os}rub{V3oMWt#3m1XXUvjGFxb($3+fI83yHkH{JoQBF? ziZO2?ca?N%%z@O%sAdhhUIVs3ch&*1JIdu!#R;QPgmnvAF)MEb%sLSc^ zU9La$96LfTpSHs?Hv@7r$amh+WM=lFnD+^*C*kQIQFMW&fl@7`;R7#(A!I+`%?(-C z@yURXD)eL?qw=9o%IdM26=E$rR5BQegS+}6rL5|iYO=_0udv^{Ohiun-OUAwuzQZN zCvK;oro{{hNFAPi;qfcQnjjXmBN8NW|0r-e(ZJBI7EnPA&`H%9T@S6t#azIng+?+~hSf`QceDH5& zPvx>c*pLN?b4FeQi5B+LQQGz&ef1Qvd0k!{6U#&q>DtpL~N0>w6jY`=i(B#toI-VbAX= z1rpa9q#pSx=Knqt-S|)}=prGH2~~T@Or`l_GTChFmJ5dytlLw-`dw(pW)BFzO?1Q&@=?7t2k$cgWPU>G-Z5YjvIAwHQ9X?qsdu z)QAk^|5rKr8M-{(k?1|l072I>ZX3&KKXo1>#Wxd%VGe?l#KS*?5r>4XgQ6dOJ~@zn zmD-5hENt_;3NgCi<37GXGI9VnfP!+HJl_jXpS+^oBxn1~q7oM=|5XR7f5RaAfv3h- z!bWf6@-wgiLAy91flU%cPfkci+iemim8dO;LP;pdT#87>(iGZM{xX~6U0bv_>~{-Q z*X2ffCrI$>?el4rre&{$n$Cjy^fGJkslAn4iVqP2qwv)A*s27tPw$pricXtTP%>RF zTGokqw~E4+3fvYY4vO*>V=kwn+Rj3_7y!_v#)fUeUaCXDRqL7iT>+0@w`LF$iD~3; z)JXboHRBr(QKy2gyF@u&x342(^>9Kf{+Oq^Qo2b5duoYTooOV~(q*qu#YjKk%6+1a z*?}dB)+gI68|R0Paz3%}ieZK%?H%N7I4W3?`==qYqJCdNtYaIk*&#+UHF`FnjHhphb(F$$6ZE=}4S|#mdJMh8G|j zSHj&QL($@-h^}FwgH>1Aw6}zk9>s%AcMkYu-C}kbQbnIA4n+U8vyeAYF#DQUjDEDE z#rr&DYj+j+!-7|9jHr05+vR8>_cP!QwA%=~ zyH7o{eO)^D-`-$%-CQ~v`5rv=++8H7_l#M+A!SVkUM|d^)*357OQ@(fHgMAaIVm0y zrZVIK_cW#LGYq(o$^Zh~$1b8FNSS(Vzv5oRNM$WUREJKL$sung746|P1u$!L*2*&) zjXj>?=}rt=5!Jy6EnB>>(Xe{`^NRJqUBKnLV3l9?W1Kb_P>KRf+V6}Ltr!k1lWd8$ z+5d1D(Y(FXo7GU%OS%P%Y~ANQeo;)Hw&|TiDa? zxoEHMcysm9Q$&H5W-0qX5#EPkY0vWR4|XdseJhDl zv-J2}Q24*l)SOMa8P#nTDPgZ<)^3+mqKjhOrMLd}FtKw~nMFEJ+Z8(tK zh?qYQ)Qxli9ASuNs~=awJRfnHmt-Ul3EOZ-*Bdp7Ly3q?K0J?|SJN%)Zc85+^&0Re z19bA-q<2lD3)%tlxUCFkSy%xDhB9SDwxfOZg16l86{_`R?XwIm=rNPOkjw9Tq`nF6~JX-4#35 z{_m?virp7f{-2fJ_;Fo(3IBmVOw9K)&d>khL(9=eH{%UQHoYgG!n6^Y7N-EB2627x z#7aDfVg{gFg)In04^)mIZYF`S!U>Upaa&{PNQ0od8#UaRAs2y>^bMeu@zG4S^06qL z=gdYPNhI{dq>v&Wpi}aOVOS;!l4+1SAmm`gB8#6<1Z=GINIq@z{b_$gY2NY1Ap(wB z{)Jmku=(02Fp0Z9N_@8)WST$SJK(?>6nRR7b=(pTN2WQsgc`o|D50{1^#X66{%uh> z5GPp^ibcCPd!jxQ!JOHk`*jr5c~XqCH70?>aa)fp^aEV!ti~`*4hwbs+D~=Vzlr|Y z{AODU<;E01xPY>2M09UfwffvedzV;eAH zS|H%iK7hB;fp-r*UtPzd<5Zwb9?lj|Ny$Jns!2TEP9u~}LF|t*fg+T+yh>{&HXsuI z^xQd9ZLhcHw!yUNzqNGHe(n}ICNtV0tz+6Lrmu~LO|oeBEV2D0O1Dni)3d?70xhKO zk*po4eVsuXm}i!=jYuXQ<3ZN^8iBwKoQo&`{)(QbOo^w&^VA8+a#?O!e|kvyOe>t` zzg%GPdSMaleL3c0azqy50RQE1u%g%PHFbFI_gq|K$o}_UF0s=?p8$_SoT~XHXLAd4 z4TecD>aQMh^v(MMuxl3CMH+tFOx})#p*)fD+pBr5W`%@GDy5951`lr)r$&!xR9|VZ zLr$Sonpvt6Yx5qrq#}RBfFe&y;WTfQiwhDPJaQN*newCeJ+=EUkOEiTODM4Jz!4Z2Rw?4Dy^ucNhY+#suZLOzbDFSe{Iidq2Bbd_Ip65n{0?3yD#d zt|OD7i~?M##>M9VDfFUFq;(}+g86@q)G1}6_`$vUz)3r@eAX`nDWRLPO8-rsKXY@w z?#uWngq7wa0`7NRGy0NGIee$~-MN*ewOcx}dIju_3(uSyEo!K1ypGNM;-Ned9c# zQYS+2)@oVsWsJ~$`N1i+%j{-~P7Cl0py6Z=E&s#oc%ABX24 zJDMI?P#cZ_PHgL(+Q-V)q2YhIw8%x~WYsVc>LB2_u|k3VHAe%_Y*USIwSPP*s(qRC zDRz^M@-lEqET#J^-OYD0D;`b;S2T0|9$}eZ?}YrrC0V*nSlD4cl>fb+^W}EkgLr(r zCaSqyEsvNrMV4tq)K8AV>5n{cPE;i|JclF_0;wdU%mihVIfIiEsVpoc-;?8BdLIat zH5fpvbFI?-Lx#>?VwM^Oug|!iQ^Zoeijx71*Mr;-#7coTJIR&GwKU6$JyNP9+ zwbfms-m``WDTS)ee;Z>>b)qbEQQszl$VYn_%3Pbfs$tDMZaYV$ZG*?5;b4~Vin-=3 zrq+~pOYd@rR7!GXEr)E>=l=~75P$6)dGz9UIqDrK)0TA$X@Jh{AiwjSnZ-o}$B*0X_4ZvjNTz{MUQU7ZJ0 zqokCKCgJcZon`IkCJkWl+jOx28^qr-dB@e3d$)_cr(J4?u6>5dEl<7;zh|-3VpfhH zG8HMRDLlit>g4LJt6{{#&@pbVm!rF5*z(>4n4ypZWj-mfp@PmqajiVWm<4By=GBuo zvJ^6+8Gq%0S*C0C@GDc7UC?W38&ZzXEm1qq!?py@8KL0VUFf^%c%2pl`DX4dK$i6U zdU%$d+U4XW<`|>-_TT3z^J%@lw4%k>?J-izVgDR6J8?3eh2ZBs8cZ3e5RUiCIwUWg z?hL!q=%PcbgyiBB?b$M&YE#-Ts{xs(I&z1S2o1F7o9rYgDc%mS_yq8EKYQN7a?Pn? zN%DyEw0q*17cry66uLB#6#aooYzV9XocSql8MavSLM!fvYVgelYy~IeIm*o^a58&% zc`CvMG*xId_)c)@UZPiO<;yRSJj~wxeJMLq6wxVs11LQXfvf12>f)49%Tz)S* z>79o#7F=5*7IK5@s~cC)@IkiZv6|T{ai{|h=qydT`^o}ylgBG-2HIm31Ukaw3{2}@^aCN(0rcqD#5edSMeO$!cV0BgwU0HU?#Wr{r~&cey&*_^rm@2JNxksljE(5?EI3 zn{->V*xqqYNH!qcQW9k3ci;-Iu>ML(Hi%MPH(|xz?-g7W!meWGB zbp_xXc9*3ypC$hr*srao#9gs5=^~>nh}{VN$a+BrrTpFJy`SCPDkNxdP>&){;Ny+l)WwHz^W;^c0*UH#gn!QR`FY&d)opp1zM2!yqP$ zlW6ng55Wk8;POEzG7qWpZ$x(TnD~}rkluXx?Pi!nzwo{3lA02aV>T!(x##wqkx~cdTkl*6L5h5J51YH|U zG#hvh*bY}*Gw)c2w221 z@WW#l=cxv2AprBwyj4@?@O2^xhz@Fx4SStue2H!2gt|}h9Jx=fCHh=*L5%JO&OCk- zLH{1Wp5MyfWSsvMnw!xLrAj>BnP z+$IVb<9-qwjqsWWk-{1evxUyWDvWbIhw4++ zfL=!ur@;J~o?Es!)!}j*V8-loRAPAJ4fKqsZt8=NcW;MCfDo*7nJXzcKdvs^D1K1*c%!iKHU zK9GqPkwg4Z7kTqz+H~MY&eSVSmJ}~%x8sbA*udxSCjRF_lKRsQt=m}+t;8KSK5M*_ znFm}vynIF`-YK+0r|vI|L3LH$nN!5tn=mEVRzpwr5%c)=nA2Fe$kbT+);HT~I&h}_ zC*=bcTr)?YwzEYw&|Y8^^XKC&BJ-bZSjz$v67cN)YrHp?U)}={__D&)XaLH#@H)08 zVrm^!Zo_S#pe#019rr`$SfxTL)^z=|k_7Yk=dE+(8nZwIl?-WV7bcFSX;$gmWc^{K zz$M{y$Fo8u?_Lpg<@T)d!7Z`S^S_a{x7&$1zLtd@j*u3rZ{N%&Cl!+@;iD+_a<*!6 zwn8%c154Ax$!#sw^T~lv@w<7;e`e+}sWQ-l9csu4vW;bbIv~8&8NFX}z3-{8yiA1= zSGaO9B7@mD{=f8jLbBuTRd06A@L=Z*|1w#gFei`Ij4G?9fmL{5c9D@or2@8sAP@~1 z!tk^Ngb^L*8NYYh*}kXiq7}sXiNQe}9()t8F;Gq?)9FC*yG*eB2a%PfNe9vt5LcQnikovFdoE-AdN$eyJ ziL5gcEts-gq0W`zmc8m33v+SQJ$NUE|IoDqu)7;CT9JVQuh5RKS4k}Gr9DbBQUhk- zaWpC#*bg=Tk1rQ=|08LP2uM^`o|H;Zm{mn97CVk14s}kH`Wsgwk+OBXppr2|=nC_J zy~JuZsU5)dn6R?cJU)ZdEs>(WoG*D)Ik1_LQP=i+k_skt1Sj{g zvLuD%$uUoaic2E9UCYu}fEmM%_dUSq<};x{o4mH?Ji- z+Nr0^zu@7eIKYH{cFA&8@~0+Xeuj?ae{-fZGm~>u#m7fc*Vm~(eB5aJ$TTv$-PkGe z*) zqxkQu@jQ642p6Ar^MkFBury?~56yZnbBCbmqw|^p`)%2!Omb)pWGGm2Zs$CxVx;ZB z2JD#l3@FC|R(_H*Ysc`gJR2n;hfOho?bSj!a&G8#zV&SkOcQ$Rt(LM{|F_PT&u!Ql z?*jsN<}Dd(56b_7Al`i|v{SbyrR5LeT5s;8rHgc>ur3hQP|?7$wh_kDuZ4;TN^LZw z<>NsM(6QQu4RH#<)op3dc~sYIY``WL^{<}L<_*+_IG0JQBIWr&n)cW_*9JWD{9)8f zOagm-Y8STZVt}BDk=2{28Wnp#xGI5^i3E1EFM1l!^GPSkn#+9@_qT-El)|MOia3~^ zFd7pR(=9zBTVtwGqMkR&`J(AiBCsnCPbKsqoi3jx@14&v7DK6p7QbGwl=cxb!7K;b zqafj-k#_{S9^7fUH%=$J2P@CWXwsLr@Swu*qre+yiM$1bHX>DQe09ERU3_W7&3V$s zx`s@*0`t6$uCy{YqK+j`;Rw;lEsa3Pg5j&Gq-K@we$xD`)AhIUMDObENWSiC9t%GY zftPDkkXi8OE?%41!2%_KP{5XhQ5qU)!EV{!KsgN~~DVRmiFa zm)90$YqCYX^sUaFgN`G5ZYmjOJl{7mOjuyiVnlXwpHHNuK0gadTD29XKY>{3>Czm3 zS8_!#`m9l9^Ce1eF^EiCH=z-KKmSosGo%m+U6}ImHGwUIEh%kfyM7of5l3Q1^(Z-E z)QipJj^>#0=+04Vdziit zX#!=eaS^t+j`9b+2wF|pj`R3Zz8j9W!Sw*0Rdw+436yzaLXH!?WzSKQoyWKudezg1 zjFpuY?)NV1Z$i&2akY0D=cUbdko2TzPP=wlgE@oje{M-d+%lJkJ1+oT0VLrov_6%8 z(x8sN{y{T0Q;T`ZS#yXm-vwz@SWNum}TY7sHXf|5|(aKI`e z-qF_TUB+oH*Vxs!#rrks%zuUK`0u$ysKHvq)@ro|?%%=>{f|$%wf8De5l2Im^7LNN z5Uzwq+e@DeW49ZQ>=26Xu8+X5D|dI(m5D-P@LC&>b;jhq8n^3Zp*|gujR1J2ZJBf9c<^b^Cs!|aUaGkUG-5|*XY`Ru~>9kM0Yu;jIDAm zQi1>!FjpBi#XY9Ii&QiiJ<8ry*H%A)5ghN}4ISPvvngbT!j#2)y{6#VZ^OB5)iM;f zeTkIZtI9VKqFRkCgS|S=c>3T-TJecu=hk1NQDI9w`Z;4AUgxTDrig=6<9~RLKJq1J z5V3D4%ezBBv}I$6cjV-RwyQJhiFnESty@jc%leptAXeyL#_%2$RBL8EEjpkCo<7M1 z`J6xb`!Ct=wB25sRlji_mM`7CFdKyp;S>@fS;nIZeU&xIrjS9_s8!DwrK)>C0$No> zn=)HgHXz}!15(6l3@0LgC>~En`9};W37F+FwS#@C33ItQM{l4EO^*esEF+L2fvsNQku;@4TC!Oc z7%c2f*MW$EPl0Tq<>PD-d8#Kk0-fSxQkh!OaSF59%(d+j$InST=TklXp@11NF4Zb9 zvlCC(eN6MUeW<0?nIEM6+2{_Lmd>P9y+9!Y;%xHoWq@-V4)@p&_!P54Adm-lkW_cXa zW&pg_cq%rdo+$b?FlLD;VFZ)k@S?eOl4LHQqnV3Cyg;)QhUfB2>`>5SOw+f|A zqY;%ak|O89XWD0t0Z-_*)VFRu6l{S<+cl^H=r#Mr?CgK!aL+F>@QYH3N@J1*bdHN*jl=ejj)VMOp=;hZ)%<}^ z7HKD#>=&V*Mxb+lrUy1cj7?Bo924kYG0@ku&2u3)8vkzbu~hcW6NjSC(TA+RbA01H zZr}es?;05msw(f~X-t=?-YtrprBpUdQ8lqrEIpRhYdt;d=@HbQ&fL-UaLwi43L&E)1HOr4%$5DF-i*$j$C)Z zc}Tzo&f%lSvB;@@`^l(ru-#6x{nWdx=-vDl)O4JqH$SOk^a~%=m*l#yrSp*1{anGH zHQ_{y{*syyOMp)bE&zLtHCph3HNfPMo)Oo+eHV1&Ol)4b4Oq5TThnZGfAvuOio{kK2HOzsAEL=La-ma8va!1+ficx)}q0TUK&W>7%ogH z<>uw!ClC3ouX`c#mdb;vJW19^<9M&#Qr@Oi3jmqiI;`AmjJGGy!vyGM%D3YzbPG(= zW>NaDp~pnap0#gAiH0|t<(*Y}k!=NXwv{oe2u7_EU4N)Oj_}>w4 z3b+@%`9{Z~clEx@KPbm36SJa~;NO>e-&j%nGtXsLxY6^spHC`mVJD%Rcl=P8hsc#OfYF&mI4d8q1cubRiqFnUNGpx)4=_WBrRu zb{M9>dTe;aV3RYQf8!&9sv1g)NoX8vuz-oLyNxuZzpWI~2~tS_gtj@qAdcVXGo>XZ zQ=ci>!MjSu2keR>5WG=o_CG+lzaShR5>f14_6X}^m*jAqEIVddxt$FkfsPtTpv054 z_CYbt&&G8UAWYq)o4>&_=}4-wF+KEuY4U%bnyEeNznZUnpDkuPiTh3jwdTclp`lFN&xWm{(}#dzCDkP( zE0)!h&+;Wc?z~~D6nMZ?RtWFP&xeRy)A4&A0_{Fdd}1}2Ay!`;*iD~khdI=Q%@K7X zLILN7(V~@C5eAbe@#$NH&11RCn10_Nz!Rr<+?K~lN6P4O*9>H7p=x}q_$2q;F9Z57 zkh~u*Cg%JWi~c+Qq4F=I=kxrA0CXwFqKozH&o`aDH=eUBdrc0tj}{^w4sDP0DsZmc z0VS?(fAKXi{v^1*60OP!QihMJ1-o(99W9k)n2p5?OUdtiBc0ppt2D*d`s^$ko6hiF zNseT@4M!%4SA4n9fOLg|ze6nJ)HP?kIa45b9_O@8;elaaP>RI+_iIF?OOU=#rz12Y zv3Ox}WL&M`;5r*gBp_;x&h@t3wWVl;Xhq&`LyDdFwQz7)=*_628Z@u9-S&NztK{Qz zPxZ(~$1=II(y5@}pG~D~#BgVh7ZhWu6$$Qv7aSrn*l(nmS-?n_HSz~D8OH2{(3iZ# z;FdF12q+`1_Ph+f|56Ke)a-b0hH5yz?5H1*lSPSz+@}fyTb5r*FR5ow*?Un`%2sN1LNws5hDO^ZW*Z}!*Gy$-T}g4Dk};h$EkxozobK-=u7;C@?rE@!)%}qs4KH4j zNzYCfi54lwTV}X_#*g)%o8bBE$Eo-En3Jpf2d3Ju@y1Mg4&4Fg%pZJ_dVbP~6aT91 ziwcQ!DeNOp1!aSqBNT%bph{rB>X13#5pfz~YJ4fiTK9IsMi(iDaSoUd)T**kyk9D$ z$$H*l`P`N2`Py2n_tB^HE!Q_#{{hFpZoitL<~_w+14J$)An-krzPwy%U4~9HB1zzF z1a`2WGfXY0OxpQ^PDBb%8#(DmhgDn??$~$z9^n{8r^OJKf6k7thr@rk*xBFZ!`TS^ z9(l5!GJAsf_B@(X3^WP5Lyf;SYvsF&>hP#Dl`XTECBUB&svouPCLO<$edeG5KGoyLTS678`>46c6%2@1tp-$9zhg_m}6NKy4jC z0(-s!B2Pi7W!YI>wGSA%)3mKZP z{@g8MI}xHG@tM$zt#^Mvc^bBVvBl%0^C6LeLb|_YmRXkUBqfUwIBXJ0TNM#xKamdK zdp5>BVW>b0A&ln+HA*8_sm<)lID#Di%d&E97hoU+!5Lxezsx<|^&Wy0y~{^7r!S*p zo67Vh;7}BB&y1lUDomZLwGcYF6 zl%^93+*?hGZSBC{;Y6w-#EGtmp-1Bcs*bLwRCeu(5`$j3<}$R~f+rln9TM$L&Re&? zzz(SQt*&J&G&`Nr)_S}=O!X@b6r5=MYQt58Vx%OxXdXt`NIsSoRuH6dlY`}7kblux&W@^}ir{Yc zP*mQxAlF?;E15pzK@)X>rErmY!V)o}W`sY3$`1^Kp1e)uNopC7H#7}dPinu_2oKDL02UF?{FU8xH9L?n`@IfPQ#|{LMmfFk~u^9TZW5Rlvv`*6_Rf#TXuc+1Zz8&c8Z2@D=~!+l>Lc97(bxe1t?iyU=*XH z-}2L>Yb$#7mt}#jKtrnu3VV8Btaj)B$v!+%@(rBIea7t?oANLxLrz{MH?&InzHBJz z-Y&Onqq0t{C}M6}mlcd_&d%(0eOI7MT!S0A7#=3tVrkh>XXJNVVfeHLU~VhY-7hA= z1XHZiz;Miwqu;rLl2Q88=ivQu&LXzVxbo~-qjL@|VS_NgWME$4;LYDAG_gg)xNXl; zSW-0hLJG5g0O)L&;PPL!K0M!)e5sXu&iT^0UssC_J;6&Q3m*Hfnd5}f&`qAZDe)&= z#PdH>F>dz!`4c>`9c|gXu({7=>+c)U?4Ok*{4;XnFo$saX#B&l7&!HmS~S~$bEbO3 zOg+gN-U_y}xcFf)P{ez~XC}`3RMId5sKlcB>7R;OZF5mSxpgGqW{R%}+c-Q@7UI%O z+9ofXe^vzrfj_cbkBze#e6vc4m$R=Orzg3j^OESKv5B(69^3`@dN*(RP-uP5kzf#a z#yQNDEGdpv2wql7Ob{UG&B~c#`DmW*;hvy*d;WgmGmHPdfX#w}1sQQ?<#^pJv4(f< z;4rb__m&O(^iccw9JS)VCm8{6V-U~_2wl~-om;VXrE{r%uiqm#KaPgfeGA30GtTl= zXK-(0?HeUpa*eKrjz09z$qBc>xpRdU;7rdirNQt4e?DQZSjQ23gZJxixa=ic z=@nuT{}FZvuh7o41$+W?FE50^;{H7h1vIjzjKQV(T8vuR>vHTO^!VMvY03CKVO;pg zVBsYob=73nV@d^Av!c#ccb#v4dW;H)2VAob6n?%E^}I3)G~9urlb3jyC^B}X*Wpo0 ze@S)xh^g`&3Rt8H5C&CW!9D7c;{B?Hj7tvt!h>)o19g(&* zJ3IS@yg#xq?r)dL*DeFVza+IuMWC@=Wl*Z{ysVRAerKPD2)Y1%O)mrN_y{Wh_xn|I zt4VZ`WQqxlvA2&DinWt(HSg08Nkl@v8zz=QZRus70I=7m%X!WGwdQlzU2(>udZ4c6hELl&oPYz%}57dvuk~Ksz8;_>ljjITFWGZE88~;?6=e|M9CY$qS#z3`Au9N6N87LYod9b`6 z^6k779eREWt($+|vvV6@3#{v8*3*{S8N1kJ8Ei4@@JR&5IBxe%Cwbw>BGtj(w0eoF zus=ILmM!Bk0}_ zf7m(MpLz5+-xBEIcn9AoA2uKCD2r(kifW^qc?+^^4+d^l{DEL)sN$}$P?2wn`C%QE z5`5Kqs1&v59eSjZx&)*#zBXd1+X%RWwht<^QuNG%ytcBtZ|w>-p4}D}h#9q(WA;`5 zTemzv&D*N))I9~Z3&qfUwCt30*(_l&w}98TS3<)*f+`5WbN74y+B$_gSFTUkz{@K- zqXqVf&bh$?31#W%u`Z*aO!+e7Jl$x@QA}m;&E+rOTM@!g9PI48*?4a{dfv-eK!`B~ zn@S1?`7FD+h7a#Q=82L(1DIV#qqWqn^v#7!p1?l-yMW`ER$XXCGLf%rx$J0hH~T7* zDmrqh&8s{-V%|Q197;a;mYpwJ>D=w7Y>r?ucS0-H_TIc{eZ=Jhw&4izO|72b@>!Ok zS7|q!@&LfGNK{Rsf@#RZW&TTPmf+MlM^A|cMH8Dx6o9}2&MK6<%iA|oj+>tA?PkQL zTkXT8ATU9CiGSQcD+&2g?ExPaA1$l#t?Be0>-q{^WJ~V^gQ%lP^RZ=1cajZFe0-vD zLU5$we;dm{b-L0fB@nuCSLD253C<ceAlLjPWtM< z{Oh)-kB;8KX{R+Zu|I}Gy{uX`{^7ef==<*c?J%?TMeSgdxv3qXJxM1en1GY`hVudh zJ^aNe`lirP#}%ibGy$tTPC0b&QNP`3%<{FI@|Iuc32%54Dr?%(8X6i70z^3c^fNg9 zt?yvs@lV1Qo|PlZNrS@X6(||D>k_~?jfr*~FCMF66?`xOsSD?<15k))Ql&`ClEd5W z(~lWR-t#Y?`qVD~&D`i8!yz4V|Jon@(c0U-@`*oLj(=6EWE5ZFeTAq%;VhLC64M3@ zi5&mHo!`5PmU4&!qZ6DP=x#*cF%}eC zoVM{k8Wc(rv{MA{30~sZRjFHm%YZfsMUc>KwXa)t+R49n(fdB|pG&*o{g*@5pkMy` zzp?uMA6);I6=m`6q+cwX=}j{--bOU+nx1B2)oS|v5+SW~v;HS0|sy+_z zMo?)B)fq*WO<=MPW^5&`Wvg+cW27rrkxi@sokYXm82Ww6eb-;ho&WsL%wBsXwwxo$ zQu;Uo3LR9`gb(rFSsIU19_0f{4508<5dyVAhY+Ifd!h|xX(@H;x@qStDK=?~7IOJ}SW?O2xvt5Jk6?QUp9U zSiCRkv@@!?oUAj>bmfbFtM!qwlaBq9*S+s!cQ5UN_dgD~qPUG8`cUhZ&-~l7msQ2P zb(tTLiWY#9Krn)+l*$E6NaCBz2~v+hMdrP8uZhJS!J#Cet*50^g4HBzj^K!wzLK+E z`D%1y%o`dO;3+3}(EHI%Onu`EOkMFA`aie^cI-rTyQqGNs`IQNs3;jK3LzF&NnKlgX^)I?x(kT3(g0!(a~tw8{_`iAVO#5O7zOr zOh51-<<3bGqbTw@Ov_-s#d%M=GeS9A(n;HdUUi4@s8Llj7 zC7JQII=SC3SXs%pCeA*)5uRDVq2-}pe|_?2zwx$Ltx44X)Y3nRGFNF`V__AwSWLXw zwF4bMxmeL5B6#H%OiQc1^|GR!W>9%V64YVqIq6q^o#UVLJam#Q%{m(z_B%XoYMNcw zevAIMzd|y*lWKA+q69}kI|bs=e)r#s1T}13js_hFQc;-_uN(qsFEOI#bRFU~1Pp#; zgnl|ovi=Cl^~bRCyrQVR{r4%X{T(@GP zvy$HIG^!)Dr>-Ex&^yHO(q1f%5+#9=BqN_I&`Aq1t*~XTH|1BY{ezdk^_?F(;^i;j zxwH%3e>jv3y84^vJm(p!^Zq}ye09RCpOd7WAVs`ORCduLga_}$&rVa$^;x!ToYM6P;@G|6Cbo;#pKDbyh}Wd@bqi?_tcn|Q z383(C7Kp@jm=|!~Qv^%e8Nmzmox_YTBdlD@*yEng_*qY(b>#Y3VAt@dvi*)bxaKe4 z!`w~RVYH>R1wj&0GD0`d?qtk`E+|i8RlS+hxO(;lcoL&1iyW;r);R*2q|>3Y(DP<; zZglLg-ub0>{}DW=@zgnl4Eo1j|H5P4y*vMWEQA+(n=6|Mb48a<(gp-OxSoTGRq9UA z^|=Y+q9}rz_spfvsk^emBdj?7WR8FRFLTm`FK$A5k2S&J`#HtTF6@piR9iL?wmv|y z>0Zpt6uRgk&JmK7FtG+X`Z(n1<7geZ0kv!;NoNe5HS^{lYkXN!+7@jJdfv z(ln(kOT71tb~>m4yYhf9<)4n;%X8|CW3I#=PI_hy+Zh6-S@?xPB)x4D0Ct#t#g-!zmw& zI8?mB#A;0y-+hUOWve;;l3(YP=e-bV!t9R?-eadH>D_l1vp>9^uxkr;`-6lXQ~0SF zj5u1Y1RWfr0{!kBI?*@+XhRQ0x?&Ax?UC4tRdkL!oz8KmF?#rVNSk)TM;o_u2ivds z59Y4^5@~-M*gmOJggoF%NRz0PD4_s}_hG+a9~^2!J}88Xk7ekcONd$!+D8xFsi@v) zO_-m(fWwuRmPt^=_9?9lMNv=`72^}j2qBQS+EhoM!syeU&6@MhZ{81&CLyr-)|GIDRR3cLw~5XgL>EG)V`=BFjO zTCG3wH!k_;r@yy!3*A3B=!@d+|JeJM-@Wm>?=rpFOHx&J#30IOe26hU9YDm_{jBv( zg1B3hdZ+IdSY3Q;3=4ygOqaXxS7>xgm1#nJ#oEE&OIMFu&e?Wq5ULFbaTd3(uVGh~IOk}$TS%fQ%Zgxxter7CH%m6s!aGOm9H!qV zzvoW85;{j87waw>9tDII>(^u3ZKm$L6V;!iooM<~Gqj|I1dY=kYaF32a8x0_?p}m4 z1VWNw(gY=fEpu#9Aj;#af)y*qb;#xTd+*%5((c^(oui)elAfHtJOk6pem*@ML}z3im>S(rmnk+=^x%m zl^4j!D9P9ugP}YP4^Q{*J9zLDA7Jk5pJ#bKjSe}^R)`a<4J1j1vjJ@kSu3He3Y1oa z+ER;Ghy* z3fCh^66jkxCMByJQ+M1%cX|h-C!UI=&9(igAc{4|oxt)FPh#60Kcc^F3+*J0T|@$v ziK)%#P|47sr&*i4w1|e_Evj;~({l425=}U9^ZgHw+o|cBj(p-1XO?=& zOTj^#L%;NeFMMJ;x%ERG@h1y5$_);4bTP!Iy(je+sps>0QFs(iLoBeVNJOyxJ}uos zDNRro!q-P8qO=d*NYmlAb~2twe&56=P^` z2x76i!j86Szv3;d`h{0EMXrw`!8>mJ+-JD`Lw}EK--dXH!@)>KugozRw1*^mQ+QRs zTk6gQef9dk;Sik8c2IigU;S_YZT-w0H~;O* z#GD;mrQ&RIhM_#=K|&1A>49JjG1M1`#c@Gm$UV^paY7mtRbipEjCC(~3Flt&Hgvlg zwDPFnJ(D+G&wU^MYm%F;VEdG6s zLj_Axmgvc?Oy?yMCgt~`a&^|*@c&R{HlbDy7;>PArd62%jJ1Nnzaa3j!8U; zgscqqo1|7KgIddconDmSTnyt7r=Nh zzV^sPVU@xOwYY`HIFI(4*_l3)X3!eLB_k|5^KqR1n%6Zs^G6G7nY-;4wqEvO%zbwt zzE9Wn=t@bIL1Cgsz{0*{h!K^&-J^e2_HgKYEW{DSrFe|(JM?%QZ38Cp~n4^fH`0##Wp^&$R5#Ou0pz5*#~^~ZQm@H11)ZM=eV z$F`+D%!3Q1*zol8S#!a2=_>=0&@v;43(QQ9)1fy^| z3}065qA?$QD;Cw%c{~98F#o24qMdf=b_;AW!pPZY^Y~x?4OX3S;?k_|V52P9_QlV$ z^IzXjYu9E{Dy+*9<AERdB*chHcw~FhK?~F%M?2_w770iQ7*Svy zwwI%JZD;2V-=nu<2kDB{B;(_c%4s+dDW`X_?O*;LfAh5{Ti_}S&XX7u!|cwZl-l!d zTD#+6_~p{wZ>7%D#zNOR%wN9;V%kkZx1i@N%T9Y-6MBCXF-gLj)6QVmwrxz^eJ8%G zNKAq;8l{D*EC~|Obz~$KJ=O)2D#Y>C1VV6_#MD+^(`t>-o6Tj+wAOFlzIpv!H(d9P zGoSzBxg}oiKI33A==xv(@DHCg*6;tZ?)KLcoLDSAnZHTL9E_@qE}})ZN)aUXoC~o4 zE=Y(Md(cRw$VOK({)u8zgl@%5CEygPXo{I#YSoHmN6z-=`bQse-1j=`*B=u3 z^n=Erf9Tb(KFe?0@_)=+|IEywa@V6h)~52VHstXm-oQZ-@S<=^@QT`?yI9dKA@-V+ zp!EnGb_8d={p}oi#+i*Ve>9kz;hqn?m*ksYf|*GYv80)xl7Pcgc}EBV5rqi`<5bk? z4%E3ch6Ww^`He#K`0MTl1`(UVPb7Nj5-9Q>V0kGr$uE6|=}&%`QQLj_BHpZ=)QUbc+G&Uzf1Z@rb-2Ohw>9PcfikrriTqktD0g$(PQ)Pj6^ zk|~PPA{c^GRJmnhYz!SdMcz*eTb^>o*S~Pfv)}aA+m?2v`-X$Upx^jU?>+LqYp?m= z<1Rc~T%M{FtT=L4kcg1DK;k7z3)UW(mqGEH)H={EhH#WyFfKKP)$2I-cYd4o=bhUa z^G5+cGsUKlzn}K4*CH;8z?e)^s)}yW$A>_YBqU-;gGO78@q+Pc7;cYFUhP3Y0xvJ7 zRR(Q}gAa+(yZUbkp7v;q$z3}c8}DFUMOEgp)o)qI z`)9`c(}du0cnE>ApM$ScRZ5X)gR>4LaRa@3?0x?+=ur&H>uNaj3yBjJB?PQ3z&l!{ zqa2%v{pk&lDmqCRUAK;z?b`{{J8@-!NfU~qz=%PGdHoN?KqlA|RKWT`mS#8$f@ZYU zraL=>qaw*PnbxgTsiVI8g|FQHgo|Et(9cjm;0^k{8$Zyx;_p8CmiAnJiLQ!{XonSt z(;lrg#tyvmIzwI?@T%!y^ z?{%66!3B#C@x7#srdRaHIw`Goi+>>eOONP>}Z{&G=BRN_e`SMqA>Z@K&(rVh%9w~Nu7hC`B1MvN;NXt2F z$O#&(C~Vy}C_xBTp@Sw3X^fNr1Raezb^oc3Idq8fkUw`GT}9c@P#4%yhx7)y_2{fA z0s&D9>jTOdY=F{P(9y$BZNNXU2{$r9>&T-JeQ-(u@4Vu(aP1XHIg3{*783)Zp&l>n zVyZ-I4a31k?6uB#P>8sccr_oHQMIs5Y+VIZoU)h&gRLrDCR9~HSiX*t4aYYd{KtUK z^5wWJWAgsHN#=5Fzd(W_Gbtf})X`MY!d!oj&gckX(DvwKJFC$K=S#ef(-y@CBsdvu zrK|71bCaJbc3gk#IZrE=daaKY2edn}@eluSrJdUPD{8KPoWhAx#H6V9gN=6-{0|C` z#;Op7UKg%91hQ5P?|2_bk`~><(?0$LPJhkoX-_O)n#CPNxT#6De*A-EH(iaH+euaS zNz;@dI_h|b1W1sOpq)m%MuMP%CZMCRjDkuBoc51uW2u*b7e^3>SMw2@VZmHbNYHUp zQ6CRzyeL%Av?RgL%rcfw(fjw0vi+N1Iw;%P?5^$D>%K}gvl&N)MR5MWH);dLIS}wk zEq>$c?>UO9L>WT}4k>2g#;ftO2ep&8bSTAX7rum(U-d?MK4GM@3>6e*S<)~2RKB7L zCE5s`)(Ez6_0-Ea`7(&d=?c3*K+K3@ByHhlXP1qe;@5xhna{tNjT;Zn9XAKChyLZi zc-M-%zi{R6nSA<{%2#b2B#YAm&Vs}$wAWsYc1Qp^eOxe8_~w1vW(Ijv`$gM?tPWJdFEk{A3Tifz7Ls? z9z;oJpw9+Eo5PNGO# z3D&uo0#gcKd9;|iKyHwV3j_r!#A`Wtk1-nSD}oO#Tlphzyz}nsAOFgWHZS#B9}5mt z4_)2%x$9myru?s_!A}?^hz3U>IFAHKgBtGMoJ0@Lsd!^iK2B3ZJQf=z1Wk$$32N<; zochMMaQK;zTjC`i1lWqLSNsQD`=5+TMOF0BMo@{NEG-IycClDdc~y_3jh?VaFL>R2 zJsz_*=zs)`3VIJY&!3g~y}*2x`XC%U$fHJ2eeg4h$LwO^q%AB;DT=BOzF=h6Hl{!M zA*R3o-C@}IK&SiQ{ivI-L**sj3x)G|?{WUX?gXkET4Hwt)Y8y8wn_RJiRpXCT+t^} z31exStmqPM`VMaPkRIywn39f;arVV;X6&4E=!;NbseDCA1^6g9w$~?3TVyJcF~V8pLl3;;%J;lyY@aV|A8;TVboskqyx_cX@^{gnoG=OzU2FbI z6dW)?&|aZj9OU5Y_O!Z&b$Ag2cmk3dBxsU#>pAo7@8ZaF&Rx3I9Yjpu_WwIBs-$vt-BB&p)m)d&UjGutnEGX zx^VzVm@ndDzTIu`*S&x!gEofQ{v6r%d)fBkzomQ6?T`HUmKx`n{?--b_uWUDcJQUa z_AMq62nAlb;Q*shhpPAXvAajUKjhE>w2M;EK}C&l2{DzTok6_9v=R`(I8cS7uoZ6C z7Is|st%GL6wm&j@*kPP?$*+-}auT7PLL!u9j;PoL0}8)eYAZ1B5J;#eG$=)ywds`= zN(@;g>6i|$zW(ccP#w(Fx@#osO2^E#CWRm_<-J3Pl9x zaV|v7-@(n;N2NF&YU4o+%`0WdMYI07FW~s|pVfrY9tHA;9$@lIpQb%GgR6RR*K9PE z4TMndu3cEvhUNsE_RkYp*YC|T8?W&4*tL+JM&(J?MH{Ri$r?zjO{RaAY)NHR_)Z4?D* z=Ic$?fyr=V7N}=&*IN%4z4Q1c2xt|koTu`ER=a}~1;q{566Ts*`eVt;V~*vd*Sro^ zt)$gXNL9SI8?6Z;;9R_q3mz{Dys6V=1C4J_zv$8Gv=I#bq9Dmqu`@I6(SHBpZ@l|A z&tA&)Jr*2LIrQrHW--yvxRt~_#zKKFj~Z}2x?kI$zGI8>q$r74|9izbzf;3~YT z3*A&*VH_RiT7EUk@M;%RB-+o9PHot^Yxbmxr2VDkXPj}+%uhd{JoIZm@rkq3nYmZxJ9p|t3t6XyGD#c= zQSZt^2)H`jVvHFSz1QL~fgnB>&c)soLfWR7Sk0LiT}(RCjL3ZC1h(J!UG#mo(^eH} zJ3-O}YdzjWnx=R>d08M@FN&HEX3zuCJ0C>iVZIlA$fN5S4!r#!epuxk4fx>L=0C^H zM~otfj!}e(@#QF$(e0JAIwKUWkG3KeSr4@y@NXvkV z^LG0V+|eg+)-S(}lg@wEQZMnKpnB+DCjaGcNw?jHDlHy^3rTGD3V}cwH2}26)w=Am z=wCenZBT+NgNk>VuSy;o*ZBoqQH%eB#kL)_s9Jq9+07Bmoo2O)NB9C;j_b?cXK5eGln=r~85^AtAU zaVOQbUEm6G`Xm`BH<0gDi*yC*SQ~_Z1drANREQ54rSS+_k%qZlr(OTW&u)GEt1rHl zgFr>@fY@G7UVi!bLl4~kOJf~#z7AIPX6G0mpP(uO0YyL!Z@vSZir$tnF$5z45^d;p zbE*KnvSMW2dQN=hYdGzK3zlwi2NS`u{TpA#Y`GiPog>k)NJZ6!Mv-|j7nDcERQaNh zMNj!~ACIOn-^(-Y?YMS7z8F4swOxuHyQ-rKgJD1-6|EhcnBMqBs1BT}@cf~BDX;$~ zt$rEx?gKbo;Z%tihetz5S$eDd$OsY^%w`{cclfdJeGz{?|NXQoDSvn^*aPQvJy6NU z#yI6Q7h{e%nxG7=Ryr(7RCU?}80w5f5voBwj7P8tJ}N^Ja3VmEW!@f_?A-ca|M8Mn zp1dRrdz3jK4EpE(;jb_Bws>dX*^#nxwA&r}-5#0Iaq7+@sSy>2t_YMG`pVJ|1=f|M zN;8tS&?F?QmgA2-p3^UQ?x8bS<1wb#zJuznTj=hZp*=E6QI)ZnE%;%MI#!~pm=`~A z>#2Z2Xg0x%s@`F39xN0rW!wAwfq@ZJww z(9~WWHK)L8=!-+znk3PLszh8Mw~p+Tv-pK~{tlgqiG97KgO=G_ZeaGVTUohs1w~aN zqEW%2f=B9nSKKx0{U+-Ai0}$0D(?H$Am{E-KGfd4LaQXI;!SR6@;^RFv3<)@AN5b9 zn%Twdjn^}lrS%}A`2sf;V#`nrRr^JC28SBjdwTcX#q4c2F7=@t5-6=X;rSPG#511* zq>Qu%_BTfTqWA4lbvs-1(i#4r!Pi5%XXuQMF>2abI@^28H$VJ=13vG%UmNr*|NW20 zZo2NOUtF&IIisSLQ~^XI%HW(EOvqi3tBbjm!H&$S010?;NN{z_Lxx_pmJ?s{3MP)) zuyl(%*r-ZozjqDGbwY1?24zx2DHI+Zs$ouY&__P(KR(tB~B1-gJckSezx4rt>KmOyD z`+7}}Ci}HPzx5+udrrvbe%F`Xc1LSc6t=V^Nk;GjYQDjIXw-+*%JEC31Zyj-wGaYo zbeyA~{aj9X%K7_zJqI)8D)A8qY+=M-=m4I++eNy_vYlWM0ubn7@OQi z@4@?)`fv^z4nOS-PI~q0G2<&i>0vRP)W%%xVbmXNmxy9r4urz#Fe6;Yj zJJ1;@jLES0FK%R%fwYz2T*b0>ioW*&roMXlk{;`iWo9S+?_Yzi3KFA-g@8k~yoGYR z2hr>I^B{Exes9x#%wGE~uy&~r>5xD~SpU?gvhJ0ygo$NQYow0O>fllJ$GlOGbvv;* zw@%Tj7^2VTdX#yA_Z1Uuy<(Xw-t{ll|A^;jZ|Cr z&qAtQx7|#uoQ)%gZC$k=q78RC)M3lPZR5aBnLHY3pd*(4jn`4lOfB`{95SdR;q;4M z&GIKa39mJ-4yTJ(L-oH{yHlt4SkO+1pVzlgidL%yVkwKBr2W~o#n#QQ-gMbzE0%6` zKR^4WLI0<}{L5#kxw+p>lpphvUfsb*DEm|o-Hk2g&{{A?(eL+3k_0DEDe8)7kFITi7lqdc zzDW=)ih%ZU0vn;8{Ui=A6#6?Dz^?P>4!NbZnCq zA2531z$F=~6b@TTJ$N7Rgj!p`k{G0A^A~*i!JNCFOPBq4%Skm-zS>)7x|p#qBqu{lY6O47*77=+%FOTKWe%0T82*MA4z7 z+}z~R_cg1JJBeeS_fpzx*U+yj^3vj*Bh-AXPZmrWL zG1>(z(s}SIMq1{Df(~u0cT}#V-|N!7?=}wQBEcm=YtH?}H&CoT97#I_ZAi0>_HHvd z2GY*EZ$U!{gu1165al&WWF&-RHr@QgH?VQz=wrTyN0I&Nj{1>zyyH0O&HT12W>?M4 z&5ZORTXHZ5K#oB5mA6AGonp3 z_u9Yw^FMtR2T()KeseX3OD|3Ocij9u<;t^KBZ)Ff)7BldGxbN^ETcWHb`F$RC=^=9 zut^NNJCp&H8eEn#_V}|o_UTVYHKF#$24@M|w^CIVKE$b1o;bZqspzp9r!h{UJ)kOr zxaip)@FCkMk^}RRBXG6RQ;63jlA@fTUBD3VzNEAT7Kh2FnfcB)_KN`@-G?4PrYDig zW6OYG@F9vbITgc}F%)0-+ItnX>kn~MP$)D>l2T4?B|iX!gAPUxKkal*e#xuY-(L*6df(?h{erZ} z-zCP51o6zy%-{=$b_v?oq0D-x6&CcJOAxduZ16a7q^UwHA$OMc>8J37OWumX0;FOCpl!Q7Y zoy0M(l_m{}3_u)}R8$i1*5Q6|6?1pqwzP}DGkfcINvgTn86pM|14`qR!wphgMD2PA z!zmaC_N({Ean?~)RSehWIdiuk6f-^!I7DksdEWEbaQ<^3&9Hc^cMRmLhSL%OU+edK z1*RV4f)}h4v}qHRQ27E;l`>`i%)k8Oe|#NRU3Ear)!r`*`i{^4@rZP%UgGk(^~wZh zd$X)tw+@^}dyVqZ6Bx|N)?q@fMmXukuVUqf4f}pUhZ1E$QkEo= z3?n5$ASfY(fc6^WRrJuqU_@$&fp~WiL`}RVAa&#@7U4($aTmC5GsNR zxJ=_Tv}R}M-hR`PE+OPO)q{7V%L1cPjAS?)=hIsiWt7E(sx@fs*__%olyu}?c-^#KkTx;uA3-bcKr%5%gh64N4BhmrYWxcRA8(a%$U z;O!l_EQ~Jr!L+9ZIjxX`*4t_#+7UTLT@|Zgo zP32-6m$#0f3alfYu9J3JYpM^21Hs6nxf#Nsg#i^3AbQz*e|jWO-` zSnEjyf>u=9?xp*{-AlHL{Eiv2N59^Rcv}WD8PocHpICNq#Rxnn`bPGY_Tot=;Y#iO}5R}p6 zRouXO0nv&Ogb=_GNVQL}bI-Z;&;I*SOTE6I#F85H>o2`D*|FpH*D*8oVya3h5lk%r z0O0F_oMCt*42=2UuszBMMcKzz6|HQ9QnfkiSjA@MGMi00C!E9~oOVUs=bHlfX^PTr8wt@%m zMx4cmSk-Q^vHrp6XuyLWY6*y+@8Lbvjrov4R7NbJn8KG!U*vcwGj`bFJnju|C0V^8 z4)gNh%K`~**qWx)yy=VAW>{Q2=cAOfw+^gDOL6SRuWkHq*I#<+;rn(qKO0MG&~N+R zRU33({GwDcZnP#*bxt(|{BZEYf>t16Yfx;Zi616aR7J^Xdz8{ES|^>vsV{p4sDmIs zdjR0(W`_OB4L>_cl912MQr&$kzPki1Pi2AK@(}nC(<8p#j?`c8&iuFJkk6mPp8ZU= zETJlwpU(&n=KNTX-N;7(!H!*wb4f{Oo zo=;CfnMdy=ID)rPKR{%0|8;!jEBob$)up*AH*PdxZpRD8 zG^c3i5kGIll~RaScmmeiJwyT2Zuh%1OPR|H#*aRR<6rP1Om;xD3mzO;kLz|1sxheh z8W%jSDp}JWp}Os6=KOo#5qZ7}F#p?9aPCp2-3{R0tB zIR64hjysVOu;Rc3R9)Oj(9xdM&f-FClVMvXidK@MgQp`JJF|1yIR5P)|Kx}F%P{a# z7E|7#K&y1#D-du*@ezj`&bFPOal7zqu!Tc9V;ujAi#Ya4Pg#QH z9J++y3H=_yx_RAiH6L9GzPYjeBvc}JTj6>+XDL2+_+_ zS`9t(p@ZJ=<58;V=-ZEIPpshdUwk91S%s~4T}p$m0_LZfN8p^JEKAz0HpT2LV~LS8 z@16MIkAC+Tn;=RY=aXu>hRNmo3;Q!Cwp9fiT zU1y%)@7%@xEwN|ho{3BViG3#k5(GpN6!%43B$f7(WHmi)Rn_XYF&ne)=^5*>YP0E4 zSC_hKtW>3vD3w&&NQo$FAt_QMMG_=P0s;t-1PBr!b}+H!9?M(Y{Vwyz_4ndMW&%j$ z_9Bt-nK_UV@gm;!=kE91d(L;hPicj*Nl->c4jA1ND1yqEy6Rf?y!QjBEIZq$b6z8O zhcEm1===Wh-W!^ge`ltn^7tfVp&)1>YDF}Ps3m#S)~R^!lk_AZqR_Z1 zU~NuWc^dQExcs9(M{D++^rJtg5z7*a-YU++U{}6gLq9S27|61W7(LQkCcO0YS*@c7 z=MWXqI!X09mk1HY7`z)LJB}`?*D;F0;x)#IwHfI3RK{9@$#bs!(9be+#WfVMq%2EX zSrc(9T&z_M8&a!X0#&Ux=ka*&(b18|fOg$WUVQSgUj^2buJ~EK^X|Umj;5`O54JMd z*j-v8%d+%=!cbfH=uVj6Jz5k-Rs>}ljTTFt9@)esjXjsK`;E7q&C^&%jo>PxKeyH1 z#tn&jsB$Po%7ZULd3M{LS8qBLam22gTM9v*?hLo<&|(Y1G;+P z_kZ`jM&hqS$TMSzZe=nED%shs`h~h}z=%RbCj*v{NYz1vezVEtAN^TOdrcA!U-yWW zBUbCGSf>s}5JgmxaQH>aUgxa6v&iwwfTIiRR6CDH9qWZ+fTF0VoEv7OoYsh5Fa51K zD8i*O8UeaJMd=L@YhAtZ7FqVXd1y{9oZ!`wA* zVB_UioUKz>$5DvE6a9KXKNXm$P)ZZEB1DHNm+{L-&f5EnPG;w$kKrn3s+XG~MtgnE zV>Fr#qE__FRH|rB&Y-e$#%=SwLe^??$vZv-?HRBNlNms*2>r2AR2qCQ?by?1Gt~bU z5mbcC`(~qbANj;5|8UJP-p^VneO?%5G zRF-afiGoBmj6>=u#kg1p^C)Sj9!Vu&gWwkztmzl;`oyRIeD`?HXf@7CK)>fxpSmVg zouAYhhMFzdunoKm=%7{!GaUV4LK4zfO5tLlQjl%h%r) zXtAkQha>uu{{ENF+8DGph@=wWC<$m3DUjEm3PUHFbpW*6ZDNe{`+Yo-_O@L}wjR*e zGPYcG4ULQU;L2ho?#PNr5OucrsNV~#R1(uqb1gE{XzYIJ(MLbRou8aI%cpV-&Ui%p z;&1&{d#cy@Nap-pW-JmkRo|gAoicrrEGUmb?Wz_<<0KFz;(bN4HNh1h_$c#x)?5qV zd5Itpl+>$SK}8vsO8`)bU=r2?!Eq5{g|!M1MW`aVXr#MLbZ1EqJa6E&UGYp93dkJW zrk2cGl7;cmPxU;83PNwWPmDsN*(5e{{Kjpp%i^wWXluFXJs&39ydyaX4*U^C59{TF zMXXS*O1-BA@5*!GqJw@nZwL3QAN$i!-O8AIJJ0x?xA)6mxS^@~?`?ph099IY-J&1+ z_^iMyU-tnf7d1a5A!q`cLIRa|lyT_j*sy&!SH0^4h*@6+j~hxsR4Q2%MuqkYTPdsq z6F?*o#N(AGiYJIiB-D4j5YBS}jwlf_MHxz*k63A9N4LO4N%g28pfZEaHQi1Z)y&y`+uLYu+;}F3y^a$^ zL?JqvcpQ&3C818=zD&J!eZvkx#gys}LX0U`(l6j;9PCavOml+v^b9U|LZ}d0d4ZMNEMB; znyRW&f_=d4S~ba~In5JjG;(YVwk`ghKl;WUWA#fvqmScvzW3z|o%6R>-eoR^T2U#0 zcqAxd?YuJhoK#dbIUy3or!;%Nk3?u@O=d5?jP19si^!}Mh%ulTF3f_i4<&d>%x#Gp z6^>2Mu6H~os9IJ@R9$*ff(XR3MJv~uXws%aDTRt5k@P~qnDabZzbXPDmMHB}KH?iw zsOGvk)VYGqyLYqc4R6NG%wlXtQFOsMVo2pKYjX%1qM*WxUPGuSFBL(#hzigRPJAqP zJ^#aJ-^LhgE@$wK|15jysb}A-g4>>2av&&=C{NnArAdB3!jU?d>)LES?OXyzAeIhn zlg&T*0XFQpSkRV8NY_iisdNma6Cv&BBx+RD#F4;$-UoFR2%$ntOr)HF zd~TMXz2*U>U~-x$gI#&R`-+G|tM!2XN)RHV9ML;mh_p8Efc2KOa|c>;@q2%YYSR{i z0#}tZGK)14Upj&Z&_F6Z9Sw?1}x%Z+!g{ zv)uov4&)+|+M85NLimydWmfM()G(+6!`J0aORuP~n|E^AJKmjgTkBX8(2WMV*+P@9 ztJe`{(&1}zl?rNj4Dz!n4Zki-0zmXAB1WZ;qY9po*%WBc28>2;*hnB5vIv397_=yy zD>zyeE=pbdhxO|RSe^73h?dCNpBmkq{T`^8Uw;>K~E&zYGu?!42y_|%U+YJ0uwRTPn^83gsH zlBql-ZT6%6&WDf+#L-z=W_H^Sw!Zatnj1Hrk>Rf6REYoN->WNC~fIdLtd`bNb#2D+vucK)V<&lNi zwyr+#&Hv;7m>kRboQXgBxzGOB?HoDu(Fv_5bx1q$7=?((*DXv^>$C$aL4BBx4M>b= zB8{mDWXo1AegB8n`_9)Gv^JQ=Bw7rn_Nhlak(HJ!9i;B@Ob^E}DYGt20@S)Z;)f1v zl3TLrnX|V;7s1SJKxH{fEUI=l3?ZQPv8F8pcdB)y+c)?MlNn^=PV|QL4ad2N_J(;j zTz>=P7Hthyo1{+a91+W)JUrA#8JV|2Od(#XiddD*Of{|c#RndH=7OG5MrH`iYpq)ab&j{rB2({U!MI@uPKP6E&+U<#>-pisp+#ihRJO^koFX=mhn{* zlD>Ofy-Qz>nizDRugMx&PTvQ(=rZz|4dWip=|}Y79kJh|SUN)g@FB|OC1TMB?@}wo zxelec@GZAfGn{6Qe1Lf_C1(1X%7bi;8|*`P{yb^7+r7 z^#^QhZfap#Z`_J-=hR zn(Zd##0 zaO<_fJBow*=sf!jy=NaIKKC^2e~HeIpJLi*V$sEfS`%UD%9r#9B!HkPbWXW>JH_Vh zaPbvPTz@m|?K{sY@h_9-T=bLgVgKDdurh{O;G z1VRW@orT@YdmsCG?!W&bZoKiVSyGcQgw&r~$B-4Ya!3Bp^8Wj!RS13{BTBBT<7YpIt_kQuS|M`tK-uR=lbq=RBpnv{j zzjxKHsn#1ihZkgOYKmUB80P5GDu1Od4U&PpLU7KZjerlBOi`63L1%1y(_7hg^);t+ zi0e4bXl>d;^OCD5_uYl*S4e=oY3Yl@$BH0o*k3(hyoZ4q&v=JEb_7TcYbdm6Kp}G* zF?r+c4#XH5+ji4a4Ty_`5C~#MxGtkmE8iDC5REvSqg^uyTGQRIfo*TNb=*TaVYnj) zSp3mL^uKox#SgwicH{usR~Qjcg2xlf0%HuWa;RZ2uA}_LONX&WQ4vwW*I_K6qaqMe zTW)&WsN`DXAxRc_5&&kQEM6-HjO1!878OkA<}9#@kJk} zJXw~3Tb(Uj=~6g5(zN4-jF1Ros7*X-vS6hw`ikplZ@*wHV|y(Koh6oj^h3ILeF2{S zKJLhiWYM9lp&vZXIogdDN(`Y)LCJ_oRWSx$;49ZZkor$u&ylwpXyrgFieeG(D?$}e zFD<}VpXY@iKFGG8{dJm`UI}YC4X+eCZ@PuYF1nn~_wFTxvNo4WrbK$U5n0)kS}ou` zzRo<0hY%dmM6A|kE^EE5TUGzL1<#+AQ#f6J=&ry0_{M7g{tv6nH}lB`N?VlDi7+6M zf$f|Q8X?^nUVD^E-m*#wRnBE+BuEclV$^z#|srx{a3C{d`Hd62Weh@ITBgP1jmK?p-_Ti*H}Si}D3etC%l|L{3p`1ij_^~e7|^!;BWzVKsm=MeAcEq5?R(QdZ^D2haU z3ocUj%k)QTuW+Dt9!rM;CMKtdLDTPfLgg{4fr<(pg@zJrXOZ?35Ao9P{3b8{>Bl(y z$iooMl9S*`pfxkYwp(rkZ4Ku+P!(7H@$dZR zb!Th*r!AmA^~4i5&YJATtdXgOUI$~cWZJ=~1gILC$%on!b>O{~vRoo9cK`BZWGft-3G16!&z^5nWe#afU zSXg>n=Hqm~SCHoox(j_;StF&VB-T+qG7za&G3o92vO-%+;d|DDtbXVd3))j1!cyXo2WPX8d}IFENZXf2@agqcqV$2%hHrCMI|68qYxT47G79 z;o!4~3n^#?g^Cds)KE}n(553%qd51|?joVyfyz*lII1uVH8IMH+m=WzX{<0CwlMjY z_aNEYGh-?CKFy)8e4cRk7ipCpsVPcipo2zQdGgE z=hxKpbrJ_2Dx?j!X$mr%5ra^43J4KvE!tQ@2=qFAW~L@NvfM#BU9uOtbUyVM^4)Ke zf8f`cy7nf{o^ip6VRpw3cE9@rJhJz3q*v9t8j8{RN(LrlL_{<(L`v_-f+Pn@0U!EU zF?Q+TOYeT@i6?ek15cjmv7eS1^;3WS`*k*YGVj7ChHBA7BebPUuR9k z)<>2|&LVi{2*IJXpsga{aiJpMF`1@U^vJEoXoXY`>pkwVAF}l4zsK&paCrM7(kTX(&Q zv#^6X^*6ZlPSf3Y@ZD0m%ayjMs4y6iWS1(nZ|vZRl6Gh+Bq_ZR0$G;fs({CmZNHeU z*WPgIXSR;B3uBpj%X?58w-L3b==2lG#;4s)Pyw%foqiuNX*ti~D~Gj~B6=EoE=A5Z z!+ih|@{2DfOitk=Of*~Qn0Dn}MkO@{?K`V?Eay5xZ921J<(y=1T^Z>2do-F&+EbGh zMM-Q=Gke>+QO)*P$2G+rQu|f?`n1VfaYY<0 zjlL`fMhv6J5OLKp;7$#Ve2Bxvf%brNs2850`|bh5o;5n%0OW~g-N~T1dDMwd1+}!!-nY@URc=ALfK_&1yLQFgEDwc>=Nq9-=5&sc}8`hva5(O0%I3X0JX!waQ{>5M1 ze5S{I%2)Nd$G-c9JXdefAt;fM(yKlsE$>$m6bJ%QMLPWs4V@EP6KuQr&1Y8LY$3w!YK@n&)8iWuiCZ@3STgEybJu}P9 zW!I*%nTrI8lqOQ?h!+Sd8SSVjXfNl!j(bpb=?A=uiKt6TEfX~=YNT{j-7d4UvyfT1 z=rZQt_j9lYN`hf|nFIguSzh?l-y!?agG{-SSo9MwKu4rusEo(yB^dh{;) z?89bFio=Jo58ln8Py7d-zw7gaerK#>8er39S2B6|RVbtA_j|bLkxWwtPmB?xY@KOV z$iQZJ$Q1%r>s;HsDb}bxk3I6x&jV+a9CwOW^UpH6{dY91%?Tdm6PYH2P}BBn{r1tL zPSvHAVju*^hWS}~y&kPyd)RixIVz!eqBuHp@v51Oa|>qkRyKU-pAsiFP!t7TCvSA3 zMq=$;6cPhepX&kPbRYy=@c7N!vD33-9*mIhxq`j}-OO=1)HcQyL8YJ`YujU8v)s?+ z1zAwwl_E$)#TBjY6yz;l6e5wP)=0OcaGKV;KSFEg`1{VQ1N(UC;~%5=)St8A;7er1 z61wc5VxO|=(XR?j(;z*BA*4Iz)zj`^GFUi#h^YAa9~K??BdoxyR(ri|FmxP5-$E!51i<+H@kGC?;iC03B21 z(4XViFWrY4sA`E@O+*jR%VPW^N)%?;UYHhOE34?Z2F#Icrf@}%Mx#MzX_4ajK3T8J3!nWJ?2oYrOZU+%kE^wOszb z51g|>Y!soh#PZ`$(Ea{>gl8V3_w?gXEn%!sk=UO~DNvdaEwQRKfg4R)8@Ho2ZozKe zM!xeR^v3PzsSV^)Q%NuS9Qb^)=@~Y@{}(y3|5@A%Pm^Sn6e?(xl*8AZ@4ngsSd-Il zG?>2pI@ZGKM$ODnUb2V&v)^IM#1y^$LL$atAg0tbL6U0LxTC=-LWNq~NFrXR0H+BA zDwL4wp%!{h5jJgQ@~3_oGcn!((|zt)dVljL$hYrdd3h0IJWdIsp>hr?P)ae;Y*SSp ze9hb+F%^ljl4&1r!aY)zJ=&9P4lN$0IoU>8LwD&Qy)S)|ANQ8H;Fo^`#;8X!f6*Rh zZn&8vcYO_9NpMxtuGZ8w^mPBECpXmHA1R9kiBHyUH|X|z%(Pm~ec%7lyAFKwn_rs0 z?Y5;eG2W97=r8@&Z?&ehyvsVKCB&rt>m1S7?Dm=%C$AEK#DkrfVEWoO(3+b&+2M>I zF%XuPIrQ*%DIdBY_4H%#;$CdkMaKek#FZ|k^-MA?DNBcuj8H`;vK(7>C?5ST>T!de zoTSh#WWz?{#2kk=ZlQJQ)u=5yXl~qunV4GBrU$KyE@9!uw-LT{0M%VeeapiXZK#>k zLOjQV!l@tv-Urmg3==!YBPeqWlw#ugo6!&7%kqyO#Ww1)poC#{D2%Yd&t(XtcD$IX zfkBe{UsCH%#w2A~VrOR2*WSwX731BV$9|XPFaI^weP6|RXtyk44NKi4SS-p~g7XxW zBR4I4zam#sr`m-m9#l=M?CR%j6 zT{Y2~dc&9g{P!;W#oKOs@Jx*Nqyzf>k3D>m>UG~LR7NX^!J!qag7|1Kk5Ydf2gVNR zwJ`)}O|bpuTh?@ddPNjH4*cU+IrNpkBj2|dDLPOU2o-gvd1$w%f(Dm@TN9mjJX(mZ zqKYY%BN3{lF3`gtIZROC5Dwk-X`F4KH*aC$+Bed?^m-;Pxdu7Q?p!AXZP@&-pW(%4 zo*+DMSDH+Mr)>@WvPZ}iQJ*)G;4gzj7&O~2A)DPi?!gQ(bNN-2TXtifdz#X9F_}Sv zBKSa5kt(LX<5nxjmFM;aj0y;zhFE%4mxy2+nts2F)*8HqsVz*u>wS=&6|t}5B6OG8 z_vt^U`sUxFd>2Ct=K~Ip&KoJ=8$d~hH5p|Q$W>nJk0-^m0S0ps)C-z1qVgV;s>$sN zu}!>qqI2kl!^oFE&-31}?H7I>8Pn%~@ol%W@0(xc$Im=PKHUTdq4H?^N;(>nR8f^+ zttAEr!AV}ZtIL-Teh7GoGmv&a*$66!_wBvT5~hweeLotbb@Zca>LNluHAS{C!+i8MCL^(t%3T)NB+D}@Uyhq(J5~e*(w-)4-oB<5v#Qaear15XiH#`LA~+$s^!x@K z&1OzE(d5YT8Oa)+HAL#xwHT?oeVR7I=1>$pv=Xe&(amZ47hjE;-!#5~ddK0f{2k$& zUqp&N5Qs5=4>il$0`*bPBSfit&vkvCsV6*0Qo$Rf-p3Dx3TmYdG75>)pwS*$-7d}V z-OIsm-8nW5_^jP#!?ibITWx$9P?ES})>^=i^!nFb1Zu^nB80Sik1=AkK|9xU%bi=F z{rJac&cs;tzxr4I>NWq;H~#nk$LtF~eDpW6s(-y6q3S2BzDowe1%uuCinez^L{nrf zuK(5lmd#h5TQ;f1!I#+o^}plL$NxRyk?&xu0*M}CAaNTZx_}Kr<_y*wbTH^Jc*rb4 zG!in1g9lNFD&VxoD~~r0Z#+Q@| zw*1P!Ku=ANWsI@xv;VGt;P7AnXYyhJ=Sp&$gO9|R=2aD>7Q&H4#)>J`8|t~&E9z7` zL>-4BIza7&IKJmk1)b{6lIn4(EVp9hrTPxt>#+RPqhyy}f!#2UOqS4`nd9Nl{~h#u z=$J^~Wm%xKB4s}W>iRiR>dZ$>a=98J9;Fq*MX69z`}NY7uX^`~_nz*Np47JBd*6QW z(k%F!^>O`@S1`Cwj;*7o-}6c_cjdM0yy?v+GmP=TdzQZc9rl0hUsL||e_>{EncNAg z?+HB*l|qR{k%1Fb&`3EA?@l-%rKS0R2uCg z%@~-fx|oOWW%-Z(1JD2N|HJRM=LF=HwKmcXa!G z1dHCh19!_$qBoAO9P;9wUt-~o-(#8*qC+W5l#HUPuvseI>EZmKQnoMkkq}5?GR_2C z-99>C?;0|N&O5y4h|>d23N{6#_l z=xB%)grE@Hz>7(}uBu+(NYb}QN)&yRT1Ke_l2G`C0s?9dvcXDadpN zl|N^9cc%j`%bB?0&Dg2&GN)e-`NqvO-}Pa78@3V~ZFJtidymwj8m_F)X>l19AXkP? zr-N@>`WY3<&wqiaHEvE ziw1@vgBGf&!0RyFoqLr?e}mnxb$ZQQzr?&>u~tr>cwjje&=j&vqaObuN51=yV;P^e zmd!W4k!*gR7=?b>qt$Ap`J9mAU7FzQW8l)}Q<+1cilB@_>6oc*@6Ah}{oR?;RN-N|Tg@~jTvVy8Uyg*c;R!VbqVE3fTa+OwfQR*8ziLpqu$ViV@s0oH5 z!qn6Z&INi^iKvKFJ+h}C<=`LvHqU(gWAqNbG_H?q6=tryo^aD!VPXRBQ|pHovsyaw zJcG<_WcJ#d79bMA&nQG#YBfI9fAR$=Fmo~O}8wD!6Hbk%OCaLT-Cni z5&>Ty3EJ06f3;ry*dUQ0b-=63iGvNH#6Z-ZXdIPtL|-8X4$-^kTTom4o^9;8^({1a z>?9Bg!Qp&amkI`PFxCE7Y=r|cy0nv5GTihk@04-9H~sx*?tIhf9^-4CN&fmbeq*i? zxJnBu>d})~QGgq$69sA}d#q*gMxaH==H}SAdySLfVx;%nv%K_oe+qYhhOqAmicrvr z5#P?~WsZ*NupAfYmK|Iu(FVkV*D>it8z_uW7{O^F4t(M3db(4JFsOMAO(FtOD!eW6 zwxr5RN?Xy-9R1AE%{_e^SgHz&05lp@#?bc>qYZPt0{zW9Iq=tiPVc#A#{DUR(rkPC z`9@QYcBOxM#dhQ_jM}_E&ZP&kv z$+OPbKM`uOu zRRy*3`PHsq1bpHJJXg&F3 z@@|C=89`;Z=n1jH5YR!ON*xtCY7_>7N^WA3e3a7>F=1u(9P1PrL9OWiMxEMF>fcuZ z#IMwqbO0SR#t#lboI`wtaSj`j{YjKS(4Ihn^p`PD{gCCq_&pZBckkHAa%N^D6Yu>c z+>YHi)1ewRHzqMKDD65y7v@@ z5*6MD zM5je(6hHiT@*S_A1D|$^HK}8R7lR6y#if({xL3=Q8x zT!yulzVG8rLFbYCIsC{&V;S@2ORpf`ela3Cl~<#wH?RSPBS=XQmo{fn6TQVq4h~cZ z7^Uz&={RK3U-01FUpTEP>em!apZU%=cA8k+YKc;G3L1^pum(D;8`LSB6ws62vy#-0 z*K9TMK2o~K#D#m9+O_*Mp33QiTUub@o-gA*e+T*V&!D}p8KJs%(9w0Rije?(>_<83 z|2wj|JQm0G7e4my9DCHCJ@w9Iq#rU2YxSPkU4myGr}*3_Ie6bU!H+knPhWl&*=_Ge z&ClaqAQz3raA@CQCfd`KrKi{LqjNpb=LA%(Hn-yI9M$+c?+2zAu#$lph3E{lB5yXR zs){Is7yIrC{-pGc=OafG8S!*n) zx`j&Wd$azOBplNZeYBU;Dqkl?{y!kuj{GnvMwE(Db}WR5%`=M9;X^=KofacH(&_c+ z#)#gw6Mf6u+4_-RMrOxH(2czg%U}Bvjpz5`%YqQAm3!)_)VKr^5q4zpf5Pv>O4e`O zkl~^O7&w*<5EQ{hbV#bBHV2&tRR1v5Q$HNbc=O3g=B~Se*8C=xJ6%}SDG*le(g$T| zP%Bxck)K-$Q_nyA$Q8WCIQ&X@O@HX{-WRS7rMp117Gn%$nRK~@6=vd~em;2ML~DFV z%yu6mE(S`aXkT_2t=ZYrbsnb;q2J};-+hvoKJlM$iwEdwPcQSsSgQkAw>D@@1LnM! zL9eOcB{AxKZJFqC%*?dOBy#wn`#5yRpK$2GZzox)u|YBambVjcdMk4io5>eFld;Wo zYa@$`C5_1`Ov@6ahoX;;6~@;FFQGvo!zq*eoyT|~R8SaiG2RX*B(GE8GESp?M&En7 zv0^zaqgtS1Mf=JdXz$uHzJZ?tB+J?L)4z&(|Ic%9Y6d;gAR33x6{Ron-qSQqbY*Bt z3-QV9#EGUbio$@?5S2?A7t(z?AdjBtk(}Q&5;L^VNKa%WC{L8KcD;%$7bWf4dAw~< zNCrB?JC7mInrXANV;lZGA7%SL`E{C`wvP3)eeqj&(!Kkui0{{;QfejB34j{5o(28- z^g=kgq%R^UqlrF%kNC<{bvhh*^uh6z5S8MRcf1Rxr)ak(IXV&y7WktJ{o^5BJJraP z{KWn{?l^5?^{;6zga4s{%^9CDleJ2}# z=2uyon!!%BS?u?~T2O|i#ZEf!YLMBg7nq7+`_#(rP^~(KT#}bLR_%If)y0% zx*FEjC#TyS*nfcKPLI6RrbN+KLTMUww_V8G&;JXyyz|4TX8SCEh^tW@I>h2#Uq^SA zD9b*mfYx?+e`#NTj`iYajdYux7971U8S;%3gqXPDAv#1NhJZCfzO;Z}UKq!4*?7^# zOkI2#i=FPPzV?-Zx^h8Nh8w`hiV!@FW>!0CvC3n2 zz!8W7KBQWWl^nFfsElmq#caRshSPNprv-j_k;9+NSG06~ogHB|=_AcmSSlhSL!mrS=OIkJ41`1tpzK3lLjF~QW8*MW>@ zo2%Px=708AIMDA9;=>$q9kQk-bR4k?G>m~NK-8;}aRL};YckFF$vp`xvuqTFC!&C2 zsY^3gIOhnh7V4&3n7#7)aSrtKz!)~Y<-N2wZsNdS`~kS9C`sw#nig|3f&(OLzV~jTe554W1%+qDr5MqR!F|=F=5}uh$2L>SL=< zD8SJ1ZqWKHKnx|u8+7*w#fy7sP0pO{5tFss%v^F6i&I~t>i1Sz(vIG^gHsz?MYDP# z76mb`x=?4?rNC*S?w{zLEO*|2|BcEOmm37FH6es!Snd+*b~TkQ1b{Xgqcf_~qm`vp zik-KFQJ8~Fhk*=MmNryDK4QgpR zN~*677af6CtA)u8%S#J1bYwQy$g_{L_!s|)=f3*cvF_B(w(V^C7yozUjXy!Rp;_!M z(rV^p))MPrKFmN!K%!3)yR3~#E%gDfFp7tnDwRlExe_v!<5d%V>DBD~;4i=$%S@d# zw4r^~8`=6B|8MNg@1)EpS?W|6o6%_J6rq!T1{&tG5oBdDv$GF z9+hJd5sXqu5WEj)Gs(uwu3)Y7uZHC#EZ_Ad7Vh~H{l)z>CUbhBN2}FB0%TsWe#Mn2 z$iO%S#+~>|Dc{5%wn5NQ5);0jAXU_eG8|f5B$SFF@gb=8zkd*dhk#+-h(Pd11;GZz} z=G##dYlM(k799T8SDEZBA+8UhLXNUP8_iCsqrX4y^~f1UjFHk+KtLIxnPs%|jD|J% z7oMdWlNx3|H_znm3y+mQUPX#qX&D?~ltQNnG_92>lU?!j-~7>Oxgk%;oaN)c`I{H? z_CEbTPK0=oR!CVDL}I;q0-+Z8uY(@rtE{%+ln~Gg-Ht9gY;u0iW)kM57g!& zF*%#LimcJV7(=JiLn}=q%kT>e^!B}g+HoP-%-q<{r7^RCiCven@YG}Ey@F6Va+|Z# z#au%m>XklyHO8$<82Cy{Oip^F@`gygBPCFk5xaF4?SJy?%-{MB*2+Tmbi+3r$0vHcE898m&3dX!CMLPXW`Yu(yK z{j96O=b_g`rs`xNgKL<+fO>Z>1B(`wYTOh6OEa(Pp-Ck#=a+> zWZ{WNQT}*Yf@9cytG-NkuYe0grOg9BeB`b-|KhKnvRTUst?!Cz|8A>fPjHnu=g6`Q zZFEwBtJUF-Za*39s4`vGw)H( zV?+;?NmbOCXlvqQtv8Ov#H8&IiiiY-5kqtl2@$Jw3iuh6zDraNCy^>DTs}p$^>XaH zeu=Fg_+?~b{F{hXDEb`!`WI<*7eOk5_C%FRnZ9l<9dxin8Ce(|wd;F*K@E8lkap;K@s+O}Bk)#k;F3&Eaq$0ldJ5E7e z)rSlv34^GlP*;;Tnt20LE#toR4?Oo5e?Yk~rUwK7VgCA?*!WNXCGOfc!{!~7QGsZT zG5Fx<_j_b{PP5sh(;0(W)G8=k;b^y7WJckt9?ga(%QBX_9^c-Gd&~RS{VV@`>@4cD z2GLCJxq@B);{Szt|1T0QypqM(qNIt^ZKAed6v}9HW~qWlS&Ook7zO77(XGgpR{auu zB-55W&r!$%5;$<+DC>RNT1IGLCH3M!%X9<7gz}X$( z2xc$76g4wTW1@v=Bu@_?167qc8_6&I6(Eta-(|Ylnv~`KySVd{%~KuIiLl(ocNVYp z{hlcSqcz4TDgwr8O2684UI?gAw;TkJDw0`?kmEOQWd53~Pj$?vjAHRH)qUTj*iCDpQ^A- zH#ZPU_v(M}3_%6hFg4HNeFw=k*i6x1?jp90*?u7t@BTR^-~Jw?aoXBU&l#xcIktW9 z7g@gc7M35p5C7nIsGfV8xU_&ViX!w8@rYK#Y-A7+tI^65f}EHWx^wQ!m%{<6zs*#`; z>k?*=k?)h9<+U*jof=lATN!FO1hpcXHP|7>!ATEZ9YB0Z?}g{i-e8ZyrkxjH=Qq)L z`bmnyky(qjhD<4f^El^@H6|ON?gVQzayrGL@ljp(xv$-`?ftj^+>cIWOs{&kKl!o$ zeQGwdmue&8t16k%N1?1r3Ky_?^~zVpnm7h11zDC6ywG8upgSJ>=B%hQ2(c^%BtC0Yxs-7Q(@= z4OYO>D6|r^@|f-->h3S|d^2a;2Ywdx_$EU$ImO&t-;LS0mHyX1hkWO2grbLu5gQHO z2~`Mash^CoKuRLa|^~WanU7ATy!zjZSUa7 zJzuB)?Yr>@U!cFZpNVFMD=UiVkr0V73|$442w7f>z*vJeNpa6PPvt$z8XSR6ub>jm z)aDDRF1dlt@A*0Mt=m|$lF~H}eD~XQ?*9fYUmW-65FX&Dfuw zPOLho6y1Tf}_v|Sa4qwnVbwI0Y8M(3OTvL^P6|sHrZzmA&1W7G!+4KyXZny!_ zV-AEN(0%4{D3x#`8OvuY$sE+|2y@K1y-( zZN!&eVEKt3(SPz`?81J+!alSsQ#X29QTF=ym;$-6$!w@BD|DU_l}6o%}X(nVTfTsI!@4QU=BJ} z9JLW&l_6!s?_&76krNgIdQ2?$Gyq}qHP`dw&&|&}b&9 z#A%b0O-(U<(M2aa&eI21Qto}4e8SLOT1K)YY@q{y)E{&ldh!l<{3|^g9Zam4P5PfQe_CEiMiCznNDcH(1$Ws7-I;fr_pW_Du?MF;?So*f!(x)$vu~k z;mlUU&dsyqqrbx9gOmQet^!>GP$kUutP1KvwtmOjFbY)x`Dd>7Pi0ZXPDf5 zZpylzX^1g2wrqnf+h|^QBlLT?BL`T1{9zU!`2pd`0i@_7oh4$`!AEdKk+dzulFiOQ z-h$~3v@gA!nXS8+zW8!zwAU1K{ngNU@p*cWevejffvA(poeHU22-3U*DF*04{k)br z4#cG0K2+jaOR5;J4aoz2<=@9F%#Wj>bIj{{OQ{##yh^?L3}MTqSCMVrM(^+eLA{6 GC9y$x{~|vZ*t>}uP#CNGI+Isp8Ib14Y-&WMM;o|m7L6G zpe!+Zf=hZvqk=yYR}2_bt>s>}+Dva5uU0Yu-7azeeo7pr6y!9LW13>s@>gmBjC2-* zpr}WJxvI%$fpI-j5+QekF$XZK`{RSut3;?*h;dj#h_$epHWsBd{jx++p|#LmI>c-% z=g?<9PW!+8A5razvwdpELTje3zLAODm$2~AeZ(K!OXq=aV0wWtCbJnOf@38j49+?ZS$)$qApnTK>tBv*nO`ZH(&< z?y75lQyuw6O>gC)39_qOXPWu&Mif+ zgHnpjSYi-RLZ{y+pO~iaBeG#5`R*`iJ~4+V3gh&!l^7fY+h0{H_DggqUuc3geUny7Pyr6p|HOACHouL=v{sV&0QC>)|Af7tv(3+e;LS(ty zVWQPWBOEz=guLCNEQD-+2TQF9npfVy#C30E?y{?)(OMJt^E%EUL{I;b@6ae0ah0a3 z9F00vd?FIYqtEX6XP+~JSId^Idg7uY#4zL>j0>~dc93t_fIILUtyYW0BS$dGkT-LB zQq=S6pj4ZJ`5*@)qUm+JOi#7xyO5Xt?)CTo>wi6Q_ebO7U1J>BVKPpN_+D4ZmM zyV!_({%Q7o{P&psh5weR3&-DfcwAV^kUA+Xqe~G9 zLt!rw!75F^*GIBkbjfA?<>f8F=?&kko-*t9kTJmA!z-&NH*!$~jQNMyQ|v;WZ_aOi7aVrtjLYre-< znr+&bTtRE+E;=tg&+=1`(tGUt#Jx{ay|kCfEaJ*OS~O^jwwAuqWLsyD4V&mp%+k1U z4~-prnA*7;nV4k!u3YOl*Rg!?0AYC%?E^+BdYuA2?l#_Y84?hID0-}UL(!Vnu8XKz zZ6=yRS(JzfS}Q!B)O<6@ps(zLqkvY5X0uJVw}1)`(_kX_VheCmb@Z1F=zsiYzrAZi zFgHkyBFVit4X0K#(yLhDD;Th>heR{cOzqlpjMeJ6;JRH*^bj?oj&lO%8)`EQiD`!# zs7osmP@cZ)&|fOhrJx_Ui-T8Q$A&lFHttcM5Twy!V%HufcI|<;y`R`$X8D;Xsh;>D zX5aIahYyi$+)A9^guUPr+S|6Hr{-XNl3m9cKIbTyWzpJbU3D4nFu0Hh>jPtW9NxqmxKv)QwG16a`u!w;7#Yfm7Drf8V`Vz6HK^ zGUIr;fG$$9Q@qf|@j6CRTFQrCa_H)`mCz5+#KW{&pO9VaukGlR0g`#9peJpP+u zTuAb~aY&ep3To3TbPPlSqM_NeNUkZD7UBnoe5AD^21BDkM zsFmRfBSeKbFgZ+v1u%0P*?igf_1g#V{ob&b>wI4!M}bN!T)o28Az(s4$AFFz<&$*p zfg^|MmIYGx^mIfO{R z(_3Q{)3Xm1Rv)hgQ6RQkWA&XFAe)?E?#k;BYgk-dB+D{lj7U5_nS6k-;Q-M{<08&75&#|`LT{H@>0rZksGRb_+p!0{N>X@D<(6}&)m`vq*gVCTt< z?TjG0vKCte;`u$12NYg3UhI%vDg;dQ$-qS=wWG<&NkoKZBctI<4u0Xka_HWZ$_TAv z9p@!tQDB!A(V|#dT*4?tqcQ$KbWS5Gq}g60oE~9n>rO(}pwVj5>-9$X5w%L^s&sq$ zzA?#cXkualr4+d}be9${y6f|wKPeTq<9-qg&p)60Zn;B=lhn%`YMt=9eGW#n!&6VX z3OZSJLbCM~Kok>~Ubc4hl|#T4MKXQx{#AwZ&j$oFUIeEEuOm8E=vabO$zMGP3JXC| z6&|Mps&@o`|2JTHaoodS$2!gpxC*!L1&9%)(?niX>)kf52JbyKvxE=`Atcqn+9SP+ z9=C=(?pvES6SV;XqGBCiM%+7(lY&r6(eL-sCc{;Z;42!+&*i>%;mM5QxBv&bfeh=I2rk-gLTnOtMiW9)VoCiP~ zO$?DR`0TQrsoiU%9U5T6)*UowXKMvO!4r_6ktl@0&V1M|mQwK{1Waae&JjYO-N;eV zPy0^qq>N9F8_;7_Y!~sf-J(AtotYZrbp%~A@PnjP$Ex1++9@c;h)hk9Z8)h?z!^aV zV)R5j*5oUmyXPrX7)Zuao6`hJ6De$9AQ7Kq(N%JW2mySk$c)1FmpFLmXDAQtALl8o zV;$!N%EbkYFNn?$JAT$m=g(S(QY!7tMbf`j(ytE!v5^y|*Uk>zHk&jzZvhpO3u8R0 zv@2m)w@!8SbZ%PfVS2tQ`^2KqQ;o3wWX5p3J4*IH0QNu$zxw30RVb#ii5v(+qZL?b zSA#S*uVTG>Pf~UR4_GBaHaACeZti5pb%qdbgBBhXpK{Kb1x$GI9@+vBKqj%2c}wRCy~r4JaLp#&xxTFOrEq!hf4 z3FwFKxMRwc)wL$Xtn|e&)jrtFYOgVV(32jAHmL&)ROO&2xhx5=)2o9w`et-@TjAT^jePtYaN( z8h&Y!Oau{0yp?*RbiQvZnurWHn28x5#Co^=&foc+*KBinOhh}hckjm8tZ{|TjCA{5Mmx_{ zy*pQN;Cr5!?4#!MDriImF@}jP+fZwuM{o#<_~?fOi*=PIP)cKdF!iK$n7&A?(@qEJ;8jaCz-EiMLPbd->OQ= zfmxoV4v0>F_B-Euf8Ju&7uI16rQw&LuDt)0t`hVh01p2PR8jXz>7aein3+_Fj z(9u&o{(Y*2L)h8*v7N^{YD5nqP!%Py{x>9x$ru7LIHC`P;EBN_E@@34Gc9UCDGNrU ztp#NeE!ez)Xj2CxwQg2(B9I70pFl)JC8qYUE9m^4dtM1rPfT7zi~$`aWq@reSJYgW8}55UNO)XT+?eDtq)hj@I-><}SLF+0AP!phqGI_{uSnPtord z=rzRI8wcXi+5s=KV>s@2RpKIq&N9^tPXlj0y@Na75ds8HfAKKAgZrVg3`h1;zVsq- zVLx){0BUhRdSQW37Wk?drY4g_gdihjr5IL-Hv_y@rnS^*Vh=cD=*`T~x!^M7!ppE1 zT*~CGOE8m@u$IWM<3@lGOQMPxS9b}8I@`Gpq6`+2VTCLPfTG#VD9R31S&`=rx`l`N zjpUcE9Xat;(3+Xum+*qIDDik40WdK`q9S6FV3&9T0TmTGf~z~lqNMIN6$3iB%Rm16 ze=_|`x4viJiN5{nfPTjvMj5-!ce<*I)d`WtwARGna7BgEioDfAj3S%ggk)>$p%xL0 zZ4e_w9T*YWUB^j6R6wde;oyGKPVao*bDRP~AXX)Qd6DAz=jlEBB=Nat=s)))s^}rr zBK_qBM0qUWias_PbU1#nhT{k9Q)!nPvW@|KiF@fK?BX)j4<4kOwdrixfxh5Erfz;4 z&7HeYt%>!tJasE?ToD0XN5YU$O-e}*=-^&Klt6#^2+Ame779Fu2Kmm*n7a6~@eTTw zAcCHoq1kRy9qvG$zCB!{)Q{(&8w^%N+eQ&UbX4>E4!k%IJa(dQzbc@A{>3|OZtYH+ z=MsWotie0S(YIl8k73wz4vDOwBo+S)7ua^?6>CIecLZ9YY>puGx;?VILGs?@-2$dsycLVy{Pect6XJJV5V}@6vnpAym;r_X{kFh84QKK3V`G+6Z}`5u>H5 zDu|J$X{_4q9UXj6H{>W*wgkb7O(|spIuo!Sp&%?SP(HL5_2l=EhrUI5&8;*pzk&Al zU8q*;yxX;loLFh={6n2t5>ikfB;QqPm$nk<_qu44p-hAB^b8v=+B3%SzB=Z%ZAVQ_ z(c0H!IreHF^p*T&(*IycSgE(fK&aF0L&wr~bk%EGxvUQ8`#U?@&DN~e$=h6vCOR3> zpIsTiU~tA5d?;|0LmQ2<8Fp?1*_!mr7RWNJ*5nO~_tp9;`IHcm7*Gm)f0^j~FvYZv z;~)fK#0M$%-xE$Xxj2?RV*99lywR~S*0y~BjZzC-cUkLZ5wa~$5h6MfMY zOzgRmY|9QB)6>bq_Vo-!noFZ+K!K7GP4Dv=Dx|c#SLu@&VSIp8_~5bHkgJSpVwT=D zHz4iTthaJjkxxxCy>mAUKl~xeB)$pMY4?$o`^ylM7|Dj)mTkW#D*vj0{?vCK-Z9^_ zlf}|9fr!pteYX{HNkvQ>e2jSK(5(q&Ihb*&}>>lzoOGA z(PGisKn%w$xX%PeMLmX)NYF`VJ2#f7EoJBtf+LiXeyp(8CYHBbMh=yzBQFr2e2Arc zrjac>ksTM(y7m^PcI`o}@rnB6q0+)^+{0LdQ1$N6OOm!q1i8VMG2p$UX>xqm)1RGW z?&jOZJKz(5Hiqf#JJ|0%q7^Czf(j?(9WqG84{Gkk^1^FkmX8K>$uJl6yG5gI3_d0X zv#L`?DjvUh58#|bNhC%=n~e7S7HqyYy?)K)G+t}UP6yr0)`QR~Kme^2MOolopXlAV zhqSif9ZOF>PUpU_;~%=8eBniG*}=LV#%e;Zz~~09DgkiblVv%gHQl0zY34)(U2{>& zvLyOQBX41}rr+;ij2dq8j<-V>B`>>kKW>md`uAVq=MhlW9=MCnt}AKY@Dt2le>2j0{q5LwYXT<`Pdu)~>G_D! zTJj*i1XY(n#iJq^o1w5&A~bGz6O+5wcDj9li5)wcn4D#)I57NPPyroQ6U`$l!8_>^ ztQ66r1c_z&nrhc00sYTx==Zi-tyEcbXtr8(`UR*J#q(tMHnOReWMyZq#>Yqq9x;l1 zejYJvvnN$e&Ec~qwyf|zpw~4RPYKaEj3`17qOZn1oV9_fr2pe*Sh)L6dJlesxbO_x z6?8+udQBcR^bBC|;=!hxT5^_33A&cC1Q*b?S3-ybO>1!U2`*XLhE%|QS-esj{*(!H zD&Q$=tG?83q_u{qiKQYc!6-wFP@fi2=|qAeK#b_h!ApBl2M^Ny;rHo(=N@L>^;5Ji zx^zwU8J`qHpwUJdO(+6J6j^5J^tvduwmN~%HU>dG-9m~;!0Q#o(NH+BP5k_3Cf@iK z(C5ZIF}-saeKECR*(zz(5ex`vBc#^ag)OPCYBJ)+1E2oQ$@vfc&nIMuQ6~bNUlH|D z)Z}P&bfwiNbtG8n`xzs`pf)HF(SSmfq!eRq9Vt-v-X=YZpcLS-QIO!#{Y9j|OnLWS zUVQF{%zfnl!qjy)qtcoRFR=A)QNt3Q4EiWn901W;s7Ag<3={KfS23(Hw}~)4hc1^9@7L>Erw9=> zB>2ShIG~53$KeOQMRCtpkS88MEgV2=LktS-8{l?}uo#e^{+3LgTQZc?4+S*=)W6)I}CM{3nH z0VN9WG;!CZ%)I-<=hWiPG~1}DDZ+~@MrO%nZRItCz8u_v3Q%=nsJ-(a{KL3e_H1P+T=8*W=`b2#{xSwiP^dZFKX5Kt>aWsFZ%rX zxjc&f9*4g9C6+$*G1T|Jj$Yo6h$jS>f?m`RbBGqAtpj-+F+MUdoYqwOo#D<~qhdOe z8=fFhqXt>LaRqY#(pV&Bh_`5OSLs!!mxQ!47Z3Ph&#Pjk-BlS4RuhQ$P@!Ex>(C3B zZ~X%Y{_JB^&+R?uxh{>ljW`j+W>h|4ZAMj|-@zPGFpH9Q=}AmFf+SH~kA{2#b;b2G zcV9g2p}r=xCMWP&UN6)KxBX~+oMC_Z3M8fIS$V9L(E9P-Cr-F!Z206A`(+~6dLsHj zQC7#b7ghC}skpMzsDdZzK_{J%HCi;+WXoEOW(#xC<#?rtdTo{k&P9Y6A)IHVIPX|` zWAUlVmrnftlH^&(YeuYhl~ECcKCk$@vESvux4%yH#XE41KTMt*n(Y=6 zHH}u2BS)5L=B?BzLP#Vq33X~M;#DeV5koRLqJG$Ag>^cbl8Z>~ayW3p8)<4C6VlGA zZpn!Qrm{y!=MYD-5J6&^q(@{e(rIX-gXjVni?SLcpksg-s5*y;KfE9J;ximtJWTs7 z@1nV72Wyq1^K#I63%hAM<&nq9wZXf9)|NDnp1<>t*9_I+U?B39<`vg!q2F{H+0M1+ z+WD$bLcU>kC@8d|P9N~7l(-X)?{qlw+E_(Zd@2_TgQ6e@s8KR__U7M-A= zQV|`5md*$+PzFbofJg9BAp~rQ*i4hDjB;t2-sk>`m+t*Xw*S-rD>K)=fpbcfCCkZo zT}tuzGsF@e zShcu)c|bNFA8kQ~Jn1SJF|LnokPfA2iR$lVx~LSFCo znZeP2{1Fa)>d(>NxtB&~iCC66=a^_t;ERfh#ss)PS(Id1#vrk%YFWn>-KYdb(ySiT z*rI?}zD~)l)Y?-WIua!enc}N7n8$=5k)V9N8xQp}koeo)PG-W}F@_-$f&Zuv0S3oa#}nn`xH21;KLM%Yk; z&uV}Hm11XyntvQ<1D2J`#a6SYC5KI-TR zq{ZA|NjG>V2DMR7s5PwzJF(%PSn6DvM>6&8MN0DE@y|TMf#3f(=<9D| z^UwV{W^(3io$)E5b1)TE4ydruoWy|`l<|C8q>@#kzr@QhjF_-d9*_(AFYrd?9zT2>lKRzxfYz|L&961J99}NS13t ztT?u#^jP16(IYJwSB}9pZ16LNui<5nHHDxc*j48=y1*Ttdu>oL_n8kh{FsL48|`hb6%6%NZb*^DpD9oy3Vx@L9f>%w++hD(YH;e-}+w6?A&-x>NKG- zJww)L4!<5K;e~o}rsF^zj$yWKg&bJ7dMbz#TSZn)p2&NSh``xx+FGNOLTih)d6I9e z-)p6ow}MgoRIN36dxGW~?m-_#W7kErZ+RO&fc6lTU~@x^F{J^m!qpQ^`tUtk3cPrn zP7;2`>+^bd4&c2MOxkX_;Y5B{0o;H8{nE%~7J@3hqpDKdn)m)y zm8fEjWGcfL16pGz)^`1T2(6jEEAEem_7|6LwP$;j5=OCfhtfgsQyQ6!zRu{^ZzTTM zMx~m$eJgV>R%JcH*dr2z66^!@h(vG`XGYN4K7AveGyWbA%Log z;z1%hswA|(7V=(WgfYo&O|>RpKcqPCS@`zdEdS*nVSaEAE!CswA0c{2%NRnxOgat5 zypoLiI?gau0N?A9L7KyqgvJdkb>wq&t^~~Ki*SND+V6bmNgVqsTuChIW(GBdd%D$t)F~v((!*C zK-&!4Y!5vXM!ge8!^ZJd;#MG1WmPOrXoaHynvXu(4Bky3BH9=-n=?qguX;l{)~Yea zL3^8EOoq_D!(mXNnlfbkI>A_JxWVPGFY5=E6Yk!BxYpuavi1{mUH9M1+M6uSHbfazYXZAr^SFm{O6dOh84mpQA9Li> ze~LZyV+aK*edArvKPb z*T5k=;8@OvfN;>_0AJmEDHLq37YTwC>w5m+qll;j3JwxMzrO4o1F;5 zcD3VS5PMCsH9GMcI+dHyA+};j0uL#u3sp4|uq4z3h!G)#ps1?!5V@+tG#ck*9ksb* zH~G~!Qlc?hQ}w!-7?5Fx!a~%jV1_AHuOeQpwA-Aoa53PlLC>7KEhfbadwK4Ye?alw zuansxMeI@8K&1jPB&IQy#*z^NxahHsoWh@H+VyJ;iaG(+1(RCJ~54|=?NZ@1eN zMTNB)syRWUdG4_jC2KJAt`FljUx3ag(O9g`vDVf)+9W5bu=2sDKK%2W5)~9UAILWB zU~2n0sJ9#fi{H7Ir~l*s7k=~rv3rDmzl)QIwi+370t6LPi;<*4AWlcTj^}mBWi4RX zBIOwp$A&-i9$wr_|1*Ec^MCZOS$^iRwIj7Enl0~qFU7WPbgGKRM2qMHDngjex^IvKy?r~#9xo%VsJSG7y@%Vm4!)6$z>2-SeGNPiv^*yFJNq+NN+4jr- zf_!Ri%Y~mTnlm$qv8jtnWO$qi1ZXxaItHn_y%XP|zi@EhROPBkrNQV7^vH_hC=7Za zQ?6do>2xs0;Jrs|2I}0~p#!vc>_Xr8R{A=}lhY|mD#9wsTOBn)Nm?xr>>$tUpf0E^ zLvPzfWA=pFB-adL*<<1Eud@Ho{sWD@57SZusZxtzl2BP$q=g$mv`Db1kg_$E_Ee@y z#GZAG57DB$CJP=L3p52ffOZ<~Enu0pIq|`7(fPCAX8%_|zh;8Eo!iLNyFNtYf*maN zD?|)Nbm}e=C_|Z2)@r2lD6TE}*FoR>s_)dyb{&MK$%sY7gp8({!kQL&J_BY3bNMZ7 z{Fz^)xoy{&Mt|mEr>7C6YLT54!KkR@W$NgwV{X6G!x$?@XD75nHvos8e|Dl(xlCz= zGKN@5svAc^67>y8H`OTlL6LT7&51UJ^T}l`vUcva*8&l?{p9=D_tIXfZ+{EXotpF& zP*g*LfBK$64|KWL5=-SQBLY>_OkZ?qTFjjz2t~ocuY86hpZ#N|izNac*Xtr7lBq^g zXbMGbAM8O?O;Aby_ceDS4F7xn-UDOB$g?MP_jV*E(rOARXl;1u`F%`HOv0lN;r1Wo zu=ljya0~hTYZv`Etq@`Q`dfMS+jmjEbdZclsQPK-N~Mibtp%>AO?m2Id9=vaxDX_5 zb_SyvO1jFx4?|N_6((1RZm{ekG-haBe-j%&@=KVF=Z3QA$s%vJ(P9{?=*dbuUwm1A z%8`D4rN~6{!t*~qA(p!Vl&M@>DJ0g?ry**TGH9Kw4S}KN?9mfgv)N7(sY=smx6_q8 zN0DvV#HOGAH6DB7F=qQ2f%Lq72>V zoCc&qdjJj5rVt+tvr}6@Dyrb=?%zxCxld3${RkU==GVqf=F}4tOuX%bbPq2Oe*84L z3K#@edbE;S(N>1Cse`+1Q1%<=hGH=H`J<+MVMLeS7e4lJc4gQFew^Sj} z>~Y_4fU2rcO5wahvh1ADRDLC7+jcPb&X3T`ns@}H0;vePAgXF4!88z+Id>N@r-~q8 zXE!2q8^=AA6GB{CVBg<7qEdGx}@ zYj4K9=^gZJ8>8D~x=GoshKogsphHM(_?Z0DqYB3a(s5v*;~wkivwBF1!a4d%p|@_s zyy>mX|J<*!>Fw`1FM>IMQo7Fctw1<_Is16Xt)=Mn5Erwj|I2@Q)ikK#_PxsguieJ* z&1#F#7$U(1bUc=MHwy1P&2|g%V6yz2-k}e$<@R^7f9W9ZtDi!OK0<}l9->Whuin-w za+(wHoPXUyi69#`W7{X>8!%S%o_UPUXFo}N@E&w;nP@FBw$M%pLFiWnA{wi6G>VKS zecvT0h1Ug9QmRS^gN_#C(<9?mb>4S6j0?#a#MhftA(EvaClQg<0v3WpX+uQl7bUHB z3o$~cw@7jSU6lI{(tpPXnY-!Ds5~FfiD=EHx4fN~o_GT8`v&41S~85zaj~jxXIIQl zhOBpqh#EJI=Q#Ji1R@F%MQL*E?me`A;{D9u@)o2u!TGca(#D`h1h*u_*8`+I!TeACJTJcRB>o3qMY};2D{%`H5aO$d1JsF)VNI$2ejNE(6zUY=T0t6R zP|3d=My~frYB>TT0iWgAORq((@y>=L-~C7WfA<$u58X?<*`!+(WLX;@BBpMY(YYnd zb7C1N`XwTQ7K@`y=RPDQ#p9;R)-fIg6>3}O1JD zJL_c_pP^z#j0&%+_1*azKyoEhs1OlST3W|g_h_dqT5yTX8-qupCsUeG2E<2{?~pCL zfZX->?EC9KAspI2wsSFCwlMMj4^v%q0UhHhC_xF%Iifa{Arh@6*~tqgs+D=AJ_ZOw z1D9IdrT+hbW-Ss%cK$NtBMj#GBwnMi(U4&=UZ4tOnFXU*E|!r@s9YCgBEfr}}# zck9-zl;x1A&K8D6g=tT*;@Y%d|5mR2$j6y_-SyDAuVYOl=Z&Ww%XCG1VltMYuoAsV(5cegOS!tIBZS`HG(^z8nc`T#&XHKf1KW<57T<| zUTklk{%`@CXNV{>)6)$41u*Ih3}boxkPNnsN4l}Bg7qVnIbkCvjuAQvqe#+`HW*?w zVDhAD6g;g~i*7Z9c8fwI1(7XVuVChF??K$=^U8#NkEI9ip?CY|F^BdLe2EA+wHO3N z@g@>Pu@`x_yo(*NhRQ3!fHQ`$;|UIZ`cIL+|7*;={`zy@D8yPWeb0|mb%zYT_$g)v zhj6}+NFZv-RL2k{(sCIwLKtO}uVZaceUi8-weU{*X;bBfD{C*Tq#dChb$UzR3pbZD z-&m70GfxKaRy=ZE8d+kQhv>G%RB&2Ictf7ob z5h|R`sH#A#vlaiUx6paz=4*4P#gG?2_X)y1-=@=Dru05}LZTV(szoEw`qU{<+=feN zHf$V)_hfmRaEHSoxpB02J;mZ*{9g<|@^f7J{-1#MInEKWmaG2WPqEiS|4W}@rZ-R7 zKSZbFSnc=7+igNHU|j8Xh-6u4Q6D__aX`~MzW-~KM_6<3_M!eVEO_VhG)r$aY}x-MVF8>Ot1opA3U z6OA>i$A5DjfEoE@P6MODsEPZ9hS|2A%YN~<*w;IV+5H6lgNJZa)0jNNhl!iYu1Qcq zlWF(HOxDXLCPA8dm##YLZM+eT>qQN96NQwtVMEI176oZ%EcO;?Z=I%J6=bw9+h4}i zjUR>f=4wt-EF56*_FKrme>-Mn8D9;_txHQ3NwLBx0WlGkI^n1(JG;oy+LH#R@^~pz z?0RLM!7ZQQ#pj-8`%nJ@Q`cN~ZikiST=kKgIIv}!{ykqu9={J?b(x)=V=x$ykI{wT7r3A0sVeFvbCjuar<$$niJb07Mf_PmAO3JRB~Q zh@KI@lCc-=PN#kKb!@-+_gMJTf5p~l8Tuim_ellFj8@2NzEzTRfP#rcF%#Fe0SRVJ zykuh;WvE6w>K-?$eT^7342yzxrwytpNoICC_U#{H%NyUek;6WMYI%{xuY3;fx}9p@ zZt}LlWu_K$P^2!}O_N$ouJ>jRy69=S3}1Ofly*DAIg5r0UsPmEhbZp)8i$q_x#Bl| zkJc40JGX;#j?3Tw_wa9iJ9}>V1S=2SNwz!>xqx#7JPu!PrWjWwjYLc!<$73MBmN?F zr?YWofKAD8QmZ1Sdodb`M7GRr;n2YaY$Bdc2ij^ulee7oyXj=C;WlQtzVsf0>8;pgluj=)A!S1VYe=s59FO+{28h5^&F z({#ICI;~l_<_%nN<44YYZ1E-NzPN+c+df12gKyF5uQ0cDiZTuvR6}yf5w%!x$;GZr z>VM4UXmHW6F$f@4mC_`gQdC2{w^%h8wPY&bhYTM69((_Mz?Pr*`)qmZ`!MHH=2!%` zWjojW@;@YgV7r5ge^8blq#f#K5Y-br=Et8s6AC$n#P2l*vej7zZ^UK$l4+DSte)uhInHXHVY zr+#C^7|l<>paXgaT<+;bFHD#F7!l^){7#CS{xeo@S%$@f*fON7b(=K4N7}~Y%=Tyy zQbU)ta2A3w=twAHgX6cUn_QjQIx1m72`0JUrgCApyh5inL(ff-z2p64muwzoxZ(cY zEZz1u^uPBVX37DbmZRHS#v7&8X;W1mtD1VfZzFNY4U=P{D_b`0? zL5k;|rW*7xRh7QKVN|*ov30$(#F(^(n|lbvSD+A$r7~ zxIEot@E7n7eS|Sw_Wqy1xt!uFpTj)+BrT~}EV`JEOEq06zB^#rP1Rd>#Y@tg4rXFR zM`5RpRC+2xNvAW7ixD4^H?N8X!B$v_^WhhVKG#)vq_a5%(BYPwRECFVGuPF2NNgNPWf*@^AYqpCI^*hye5+urkI z^k%oQ`sqKx?|v@XaijRMAo_|+w{BywJiyjvX%^B783>Z%w;SC}TLIU~D1F7S@CXi< zSzP9j%wxq8i-1hepl|#UF8$~)ZVp#lj0|=?%fV0o5uK-=z*YrOglLinL@-DMBT7(E ziG;ew){K;WG)qOIcs4ZV(Xe4e(gJ&wP-fI1YLlF-SfZ_~QB-R;yn*=xEZ^}3_6N^p zKlyR8tFGRd!HXcBX>4bjxy!C%?j7%iV!&W&k<}M>!r}tq&_2qg6~gK=GVCK|0p$QC zVzLa=p25v+!L-}NZCAp!OWF3C*D-bZRWLPmVTVx95sKonjCQ*X-eXLfgS@X!xGy?D zjEIEetM@y6rAD+onn4$llbh?JKZysQ=NQBEo8OMR8h!RXje8uay{2%=HWLq~u z&vWlN_}#BkeD$-mUU(84v~HVeFtM)bSFkE+0(KQaOs$$#7v)8RM1x&Z!qae(qIKeA zbbt^{3Y%3f009>O?}#x&qrsreYM0EWq z`uk!;eM$5c_>kImT8GIpuo+@6@KEUnA_{(3(C_!@h>+VBf>M;8si`()bz<|skr=Hz zevzAFdP)^JZA&a9`M-;e)>YSV$G_YK}8x!YXhSSO${>K7|!+?G^Fc`gFMDwC#5hNuQ3Z9FN}lHfB1xvVmF48BXjF_zNpe;6z=}F4&Ct;{B5^lmiLoQTY@d|5rhCP zLI4*`o%KXzC!%c z=ZP;q%k-RMwOEEMiGJ8P(twMTBz6_78VF!X#yl9U^#V!^NA_}VxOlJ;L`~gWA-UfL zkI_h0w~*Foi0ZI5BNK?_kQghfyd+T2zweubgZt_4|NC6_!JouVPjB>THEbech=iiR zIfr;(mjSoPTP-SIV2-2RqN=EZYK)=Cj%#2I)vOvlGd)0KxQJZthav4Ym;U5WG5w$a zHsxF1%92>BwapSp6GmxAMr5>I-#APJUshNV+MSG6J14V-DhS1uuVnkje~YP?zj{N* z2%uHL!8^ag@>f5@>aItboz3YFdt`ZsR2rDdKcbDO4FnsAqIG3~VANuwP33FRqy?7o zAbwnz+i-DW>c%9|Xu2NiQdFt$b0(EL`sj&7qE$Rf8I=$tTempO!Vb(gZejlRFW^^} zHu#JhHWS7Oh%o5(Q!a+Jc%Pbj$w?@y3MNct$2aJX&T0%%%hMV)K$H}txrmXx&D^`+ zPwUmM#(nEnhTr@O+2Vdgk~^=o0d>iUyC550vIZbQP$HEXQ2B_BZOr9YbIq^+Bc|W- z&W#=(_=<(^eU0wl{VCR*Qj2*OKeB=nr~mr63V|f-ZNF7<%3ucgJif zx2j;PBrKC&WD?jlb<#*OJn^unu2Tt8C5C3V1JJtU3NHKTuQKyX{{VgCdkCG&h^9@H z936dtqgICL7mihBAcy@t+Wm(g$0N0SO-Jr>z~IRV?K#X_-pwVy_IpgfapT*f>TtlJ zFMXcDEuTUU9i(kwwZBS9L~=_QYqQsnKkVdF5?tL17RI6*qpNnK!yOG9&qxqz)JD`O zb$7B97z-g_jAE^Ti$t6HDwZC6r5MSu2wSFe++dN;;}5d@*-y}Y=BbT6v4%~-%IYfp zm1UfB^oIpiSzxWD<+7x(ThCol(k28(+9b(?t0NDhn20`b+eHOYm|wCMvO7o ztVK~2IG5FRwvJHxx^%0>aQYJ3H++DZ5C1GvFTZXB-?&6vnrGoFpCf$v7N&-MS|-DL zN13JfBWT1Lsnsq8QE(wiS;9!tcJxm~jR&P(1enRrd=q#{8a9f028)`+l?+gOe5k`> zX)jYtLMe!^RoP6$M8U*q2vCq#ilLatKlBX_9o$9v@!w$ETW>(}bKKmeVbkD;L)t_t zAj>jPFd7LZSaIVzr_!aIjoham8bKgR#G6DdzF5Yw}#ySESSl#F`~p1F66y)ks%GvfWR! z@`-;-|9wBf)j#{INN003GHN(C2#N{m8j65ZQe~0qSWPs_WNwm?f@s}yLIs?4NXSAK zPbABIFi^Esb$HFWaCpwvk%7k1{UVZ#{fYL1yVww+ea*GBue^$-SG}6SlMhlo`3T|p zXBc*u5UD6Nkmr{6REPec$4akDd!|hhB4TTGv!KMP#6%&pIr&tZQUkqE5{E;?fVGm8 zg#gt^-f0s;z!wFrd(O$gz4w?5R5j+E&IuUwo92~2FjZU^eXZM#yo9jK zs3FN@FiNO zRqCIsF~wf2MU4`oXIPd*=ODA>d4@rd5YQ@MBtt~-gNQdmv~7H=O@+m8-$s7zYuWaJ zpXRF9ya_k8IW(Gw&+lOAPycUp#}mx8gl<*gogJapx%%1-9C5?BCPqEXaK|;?ADL)q9K+7$ zt(;-y8I(n6tDb;CzweD&*-_bA>HuR@)=!iYj+;OvUeP9oeqxBh%xq!yZSQ9GO>blV znfEY!VFz>jUZ8w_C)vspc5xZ$_u1;EQS}7%gcztaQc5IPATxL(1SN!umKs_?>R#ps zF%EAGns<<`b967+j$U~!m%j5oT>9!aVP>{$^gA(TuzMFvU;Gr=jwfkp!Scc~*<1@> zw{$fTY)Abo*Pd>u@-~30e`qkgUzN|6o z7YnKp4Ryjf>%)F0TD2_JV6)6$ec5HlHL#A=RgL6@N^$g7o^s@(;0VbXiGZc*_egET zn)KllLIm%8YVAk^vFI`(WG6{x|fxdT^+oq9P}GwemaQ3Oi$CAo~Cu}E8x8!0PpdGF5QJg z4438!t4nA(ATNigPk9T*IK)|`GmD*@W$w~zn7ZUrT&Dxp)&_iYelE+8-pAn9PqOm( zy>v7ZO^G^-6UF+&6Om$_fHcWz+eqo$hKn1M`i~;f;3BDtqRxsBCZH0j!Vss*wk>n? zR}MnINAbnKVeir*F8%Pw$hL0Z;In8roA_dg`r*3U*5(6^m>6?G*Ct|Sjw*PRFyO`; zPaw-3G)Sv!ToDM~uATTrK@j5j1=h}7ezQYAV@NW_*5X<%wr&P3^)pEb zEI;-DgIoTF;*lS4$!v!r3^3NB8tL^b<~q}OpRT!3mrBNZ!fXtA!)8E&9aXs1)YU>n zqC}ia_xFn-Vk|i&e*OTfxBV^t(0(raxnHAw-u&fNbODlR6(o0H33UWK3u{)aW9Ad`ros7|J~r5 zDxnLFVr3aZT&LoBGzpBfK{@hvRV7%oRC_`@bO+F$%a_YCu69NVR@R{**7=$;-i{wN zh+mH~%Pg5HAv7z^hGP&stB?MW^47n??c0HM5aQ&LGEpWMy-Z?F!2nV#b&;}*)ilna zhKy5(xE!4*vkrrVfGD>xM zSb9)pWnmr~ahZmr5Q>8Rw|x@-oi8GLb|WH~%q6|zq?xqN`4iR#$wd#l=uK)V*!bHU zHW1O6N+|2!M|#{+OEiN;R9KsVv}O+kD7k@l%TPS`7}Y1|S$*mbUcf@eJj)j&{HY+OE(*GmR{OSjv}JH0quMdC=TLomh=HR;?O zYx4A!2lceG91Wh(G-zo!j5z4Adf#`^uiwJ#aG9QXvV01xNsGZczGUiu)vU<`tu?gm zn#)SpB$r5&AlI;gjN+q)dfwDYm`K#rvvxB2YGPWEDnN$O0Fu$|_OY44^ybm;eS<|~ znY!uYOkMMeCc*eT#}9`oA?_HC>slXc{cRi;PmiJqC+>3JdR4ExTYa~sq9RrxAYzY@ zraPKZPqME~cfCE@XC%e3_o?a?C)BB}&) zLP>EBAYXWk_x|2)1kH>QG&S~!@ffLhYsgzU)iBWO^(a>s2#rB+7!f?n_k5e;t}men z_p)tfn*K_kJZoc}qbSSN9S+9zyrZ4C(KX^RChdOAnrqR<4Apq#^0uMj91%=JO`7jS z4Wb!&Fo;@A?dOaV>YaJ0C+sZsCct3SGB>-O<>h&78!F$YKOA84Hrb1hu<*BCvi2;~ zuYG+gVQo0Ch-FDN7^DRA`ZlACJvnRVu4C=sMFhoo$>Z?ok64cZxZ!oL)68Xy789b0 z1g-UE#g2>IM>{1G&3GlD0=+>WtU<*x)$ULXPRKLRaDk&`$(}pD$iip-guJ^*?u4Q& zad}P|JhAc^IqX7SbAg+>>yWIuZC!S#G$m^d8&8Uxr;B*xQBT+Is3a*d$pMLLVyDXD z;yhcnY$2AOJZ~deE4A4yddRM4IP@2P#QxiEAr6}Aq4N!27Wi%#6AeLPN}M#3>?pBr zeLZ^g5jt||)(A$V@1vf044oTq)cC&^OD%ltmK9msp}vl6N;2-LDMnI|qrN=~7$@|L ziY=`fx+`76$_c%0!-b5mSp5DS=vO~a=iq@lY}g{kWR#7wpaDyLjcIa0Dy$P;fYj`Q zb+U;m-mc?sLgQ|4*cePSK$iYr-=-1?qib_i9aP8CjZ&{lC*m`m4h)9N*j5TzW-^Ud zNs+S+6wf_EES5On441zDM%>)ib9sslr$N>4VwQVkDtHS@Omkh0tW}b!Z&#yr#4(jH zIxMK^ODA7Bk=GnwhwZ#I9}4x;Gt;d0yZ8tyGIn{Ix@3qo15%Uf&|9q*Y6bBF2J;6` z{T*mH@6lq&{%?GV?ic5z{J!A zaO6HpS_$>Rnk#h8V-oef5+`2(O|@?lQg*!2$V>~WJv-^&_SZcB*Z-d3LNmXfFD&of zM_4$B)H3S9PA-A5F<%!lG?Uxv!-|2ZUCpKzPvkXS{Zm&Ldiy%y`@KGwY`v7C=Ti** zCDc1aA^N1)RfUQe1B<)&H1e+(DdqAKFMjc_$shRv<-VObXV5Up@RYg_KoZvob%Pjl zG?j&hhKq#56OE;was@D|XjL+m=aegpbnm~5{n4=X!#~58*S%>YhTCuwEbZG%*&omv z^*}$&oQ}dXj7xBj+@ORIl~%U!`fJ~G;&teNZrQ$dAuP;S)>?YK9-VB4P*tgWYpiSK zjxzbS#1SkjnZZ&NPpXUZ9D9j;MXQ>_DxGSwj_ z7krb~)^ITs&H9YbHM_T1mb5a9F9S0sWB$8$&>9Z0He=>huW2?;n;5^kni@&1X~q&x z^5ar#b!v8Y|4;nP4aJE(er%!Vi@|cl4_qrF#)$X+*kxp&vFNt z*>XuQntV9ym&{B}Bejt}nq=Ktd|4q1Ap~NK3=bS)bz$LThTd=<6N@1WU;jH+Zu>OT ztBZuy6?zMY*f!m!ZR;{LNo`MqO$X>PwTl}XE;>vcd#NVLb@jd}vy{=}a>LAYjtN6t zwMxEc2k~p4XYuYk8>7A%34!7K!ld+;T4-qW2_LQ;SNEzooUG-vGGQ;T8k9%ec@)Nf z^`k%gGs_{5U6VUjmb(~Zu!q$?#-l7Ta_g`*!{i3B;8&OFEzF50kmRfxUcDi?MOHH#7j$YICDGJ=Wiseh5@n z@NL85aD`zU(4NUq?Gs*jg7R5ep_( zzi&8?sTK}#@H2nG!fk)cWmYhupy;pSS}7hFW2D`l0zk!#dzq(8Dq=KZkFQ(Q&~V{m zB1vW>ATnx(H8JWc2G`0df+sqQ?X-}#VRf)VHkIRjLAC!y;+^b^af9-8w`(Tl)h~O+{1JB?gNZ>8mkoVp_rw^-yK@n9Yz3Qo zlvP#XvJ8v?t5ijo{-I`vej!sHJiwvP{u%x6f0@=`8DEttha$`IDpU&5ISSRf1!^+e zqd1b)bch~XA*rF^VqmS;bKFgkU+u7BkT8&o&`U#C-gD+i1^ zieW_v0b|U%B+9iJ%Ol#I4n@{9MyoUIifwe0`1OB*H*;|)u6-L z+4RO6%hIq{oW&VSp66+oeyjo1cwg761J+usQOczge*K2?h|uq`^r=5XzV|iSu}>gS z8O0lej(b0+|5sKhtx&oY3$&_a9$c7oPD_a(8+%R-4I9O%t4FKMg>y~ z&8+>**MI(T{J%1W<=4I9hC?U%x@)&bu``_?@ZS4T#(MN|o!>v2qfNjjX%7QcIlvEx z6iZ7@la>pNYITX#Z{3DI@GVTSM2ko<9ImOelrS> z5%lH{Vxq@}HA!x=esjFmmd08+b8V zhguQtC{GqrxmpMTLcod8YUh|T&$#KE+*w8#rpeTBEd$Y=tPi&HF+n*iPuyj#wvrd zAc#XHCq_j=MRs5(2mkDUW8WA4ir8eppL3RX?7(;X^=B2=c_T%SUNN>Fg8^-|^Ia$N zy6X(O$;_UjUv#Ug!dFUB9DiZpT;2b-j@J}L!J$3RQ#C8Y3xe2PW&YN`CEWEDTC0nc zMS(K1jOm9I%1TW|dB4Hq{yB=`A zne5vp4T+LoJY>e=E5){34672H#pRaX$`aGVRa*Can*+D}HKEr%_a}Fe;)es4_UuGM zz-k;D@CbEZi}bj|jrx(^Zx`Fs?H5kwwcXlZs+sn@b!Npm_cCL1VyKwN5Qb56>UDo0 z`haT0wUR*>3$ZAvR#xDOD^F%D4V#Qu3|Rc?=jna@7PiI#Zo18IrAyw)=@lt)vi{hU zyAXqkscocHkf60dMy$g%BTb}@)ulF9MLYc(DjgDq+sl%k{ zk}@uhQY*y|A_fb?0ehZ$>}1B#u-OQn{a^k(gD-uW?ZYlbuSc&tV7fDdH95m!LAx`B z#PwwtvEGi45_?4Bx(oLBuBL{Di*U9@i2R{vy@u9CHLmRGxU+HEYtU{GSM zC1@mTW#lb`UD$(u^%h?I)c@1O)XxD2cfUw?|1P{1DaVMEEG%Q6dsy@6h?`%MnfEkh=Mx6AaQi5S&|q+M2+G_$?_J#D;fi0 zS}+(;?AZx1ruy`T^9*J6hu_D3_YPY9J{l@=$q5E3A5ejIr%hE=Sg8$qH0yentBe{E z7_`pVjb>DylT@)rk8NnU7*SEeXx}ZmMm6!sax)XxeoxFeLAkFLu9Km|icZYxPKK7! zq{c{M0y&nre-Ev24=dNbo~Ma!`8%jNX+sqfHlzWHWfdjFhct`ZwCS-qoDQ@43DYgCK?0+lZq6n$bK z)uCf>7?z)Wh+=u^47@!J=af(}f8Tdlx#dp@FFplYjWgL}o$xm9AyMxf^e8fdN9$N_ z;^b)hnKv{{`lpXr`yE;TeOb@i7V6Px+TlfT!6rAp3Mv8BK&Xaf!NdN&9Qf3qu>X!* zA%r9UXv68FN`Ln*GV2*uL*i&-vqr~V*Qmr*(Gx|6XL;r z1P7%}hd1d$R7vqXA@wW+b5e;!iOEeb5t|v*DB=X`gwAll{yT1A zW#^L{JNOHg5Lnp#0#$!VEAIex-*ZOgy6YeAY)hU&GxjJFTQt!@_%iJ8bn6aUu;oK0D@**Ma^nobh2)~yxhSC>=YKV=2>6K*` zzIhv3olpa>hLfPbvP3a|h}qd$d^vf4Q;Fgzt#M*gt+jZKMA8OYRlzx;F|ji>{RD6M z;At6jG|TqdR%`dn>=p)tLAp(j`p|ly25U1^6|WI%Eip!_vSfANNyYOUHjv_l9n62? z&*?n(47NYO5Bub~B}yRHB{C!TU^KDOVQ*;IV3J5|ou49+V3e>%{L7Hah{XzOBUx?{ zKg10N$WxC~&F|m9(O;Mp{XW&=JjJlYh)dyWs5d~Y5#maOI`m^~r8z2Sym-aDx$t+b>5erY|ZIYZECE<{Inm05wYz`)i?DV^6Em#vR9oHuM zKv9;gcDuyrX<1>bf%wQhXffE>!Czo3Jimiteu3P!fQedW>UBR62^xJ?nXwow8a)z~ z%ve-K`&Hck-dDfrq+`}c`q6K=p_;wol4pZ;#jq^bEG$NXgK^T!RSm7YlXQf$3=yF$ z%Ov#~VCBVKltm)}yJ_*B)d%l|?|y?$xk_Zf)Yd5mp~5+b(UdWplHVpe?V^^5oe0G= zG&G!ZA_m2x*!uqhBq>=F4K4`P&|{2adTtA329sHgg=*NPc<6rS@4a)WUS-r=8^teTfKIYY-7)jQH*<{rQtBqiZ;aEZl!Ly-)oqt%d!d1>SlpQ7Qtl z3S=^pDM%BA(Nr2^Mv8&4G3X5q8_Bo{5<;Y2e5=LAbU4j0A)_b*h@*^wV8E*M^_?$ew!+Z!F%hK3DA*BWzxP|wkET{d+x>Xuly zEe>_nN?xqe`~KJIJ$FhQ!iF^*+O-243Ls)_Mz1@`EemFnd$hJWi>l%)O=WK0v%0!U z-tH{xRCZELSwz#Hiq+koigf-XIW65~_fS{NyNevk{ePXxLcH8ud;Xi9ZE`38;ke zcNa#B@X_N8(WLgP+5Q(ebnB;yy+-H#bm;Hdjjc+C!y(ht)3n=DWBZs=kRqb8&gf>X z95qmuB_RYl(^K>Yy?wz>ojHTf4L4MMUp^UvFJc`k9pyxcD#DTO8LhM7M@ZejcX(ho z`=5K}RL0hD78vY(k@EXrM;G=oJ=4OwKrl)5%>{#tGA>ga*G$S<3trb?9A1mj(9m#B zG4=k*2I>fUQix0S=!!QwcC(vW8aA~%PR~!(OP;Fe(RtxHR-SxhV^8QjCR7#OT|0?> zNT<`H-yhKH^?|xF$VNt8a?P4pWxqd23UNlrr&|=hqCGwH{D1S0erMrS#&N7DsLyTt z5!cRF)31@EH4Jzum+Q!&`-KITc0GG4V{16Wgq0;0zw{~W{^zk$VO>&}3SzL)tP_jT z`rp*gJk}~l0X3dr(@fGh+#4D;mXTuPxIco41Y6;4Mdbp)MxxPi_d^@fPJON0*Ghy# z`x$)utHh$%$kRD*DZ8r-o_~&53@CgBBFxRr0gUzBqe-EUQP@&}sp)AHp$wj)DzV0B z5vylrnO!=SaU9E_>qmagE_}neLzUfM`69*L-^5h| z&_Hj{CrT2I8Rb2Ul4eE?BdkWU7_n49#M|V~1CCOT(a_LvE*a;vs3H=HQV~qV+laTm zcD={?`>nVLE{>!~0U}x3(JPmzo_?6cAKp#lc{_QQo_mJ*XPy8_A~D7odfiFM_ULmM z3(eF?w(Z{#H^_`D#{PI`7dT-D07-sb8juP9XZU>jp z!zOhdXTC>-3RTbYi@O?wzR?W2ga_}!RjU{iKrQXoG)CcY)u!$vC0@5 z8XC?GYa-%nZkb#1dkLi5+aPm}thC zm|DliC+!&xib2VM3{i_{>e6zwwy6yb4d;fmupaeUNNtRbT5QO0RyCZF-t`@0~1s{RX6F}vFyO$GkFm7W#kw}44igkBVXU?9V%j)G6YHv? zh*c1Ylx0Dyl@nvoa>(;@b64y<)v>(fjNG|;_Q|1-D@jy7)QUEa5bE@MC*vJ@s;^6; z28tQC@(Us0yUQ%?eeqPs*l?Ou%Zm&jx|>$?5In6`ix8B-U?N0xwEpGe+jW!~nbeI; zQ-I#kuqim=ds}}$ZAeG@zOH}V+V8lCNlKq4CzKU#u*C45Z=i$jxjm8dn6lrce{c`d z&hS295Uf$EqD=WmBu}SdZMJ)QZdFxGO-)gjCAO96)Rt|}ExhuTi>Er4mzvo4;^CnefPmJp4V^|iIF|u`WmwT#q_}hK?_B! zV%pHe%NiOkPEwCzbl=?@J-qXV`4@Jwvin6eL_#h1D%OCJbq#aY zlai3zSx&#(qm#Gj4~pga-tg=1yZPo*)=Pg$gZ?8Q`g=Vk-zlc05|f;(h++(gC?SM# z2QH~4K}UZusSzo@3{2UKLytY$2+V8%t2>{i|NT3VvYX;M(xxgrE;rar8nw5EhKrSG z5HDC9p(@BSLq)`dlH%Sw@r{1S3DJFF7janP#E@mFm3UQEh{!Q>jn-Bai3qU>%*<}z zKXdh^-{mxRSuZ)AU-$9f8N{HEsgK@@LliVdL?jJXC-vx~Y)jEp(&Sw->egD?(xUs^ za~yoJal4;We8u8-zQVSp1DIhC6Cw^vmbD0Y`sFH33)F^&ixU9|g2ALYJ62!~7Nzy< zlkETMtxe;W@YFimn{O#Xu{|DGHBu89_bLWU4nbT;!xlAj@+GRgtogiqatoTgqT)qA{YxHgw*nFgqpgJSyR0oDL#|A7X9u3TMSrw{ISy+Ys1M90t?^! z264|*#HvS21cy*YrHrXgGRv}!9%VyA!$zTk(X?v}HpNRVf_P7;dUO`{aPXn~H+qzt znqBwbN7%QImUXoA7RDGtsIb;jh1A?kU&74hxns54#kOWNw%U(!!wshump*O;_3bx) zY^QBcJyTW@9~2S6I)~S|E;(+r5Q()x7me5`ND%ToN6U)+4?aY(x_mn0Z8$-C&pu9Z z_cx&!GCMbmdZiean3kauj}b>GPf5F=p`qad!N=6JrK&30?VK0_F?gmr8L_`WeC&Sk zP43fC@t%dJ9zzEMhW!Cn1QDh56~@& z`?IS()kj_4Q4xbNhN38FciL2CHA#k3KrPXvEk^1F@#{Sw3-sMhK7q26)-jknI&P=f(9m%4qHD^kM*B%MkzndxO#AlHd-UNA8r|mR;0w>= zms7}lZPq%hkDZTL8$VC?M_g`M_Jh)F+wy}guY29W(;d@^8T9S550^2%Ft=r!X6+WM z{T>pOSo)ON4&$yqNv1Z{tXlOUZ!+T=8ZKTUNWc()NHBrQC`zho%A}(A!1vH$GiR?wRoVNK)$#j5(!4Zr)lr(AzNe%%v!&@0~Z+UNUGzPq&A z3sHr+EpwD*IZ29J<4TB!HObDhjH-%64P;K}@7}@E{ynEV?uKKq^w9nIhwfv`)HK84 z5Q&q1yd%j$#@(438X7KYBpLD%X;WrF9g0JPNA^C?%99Ul*bp}(p(t2>{1HOGIHkT8 z$)HEnM52k${Q5usZ|>#{)DWMLLBH|FzrQ?v`IhfsY+-W8YOl-mmM!Qp&tx*L;HhiY zm9iS*yDKceaN41QhNDm}9-_SWJGj*)%5sQtNsY(`NP7EqTl4WVM*Ynj8X7J>MvCr7 z&H;)AZ0g}XT;$N5Uu$;hYw*1;^N&4-36sR)k;vM*QZ-$FjCA*-q#o(2s@}RMimC1U z&&0S+EF;Ky{@y#YPJ3VFJo|Jd9xdAe(r4mA#Ki~*(FmA`H2usQ8ZK(Ag4IZnr0gkm%r|0DEQPPg zox;*ysy$7%JEZvezK0(|y8U&Ta7Q$@s#W#`5MtendUPZP)#|FWc=}9@?Su{bb-(@F z!`Afn?~B-wlTnldu#=~cyzgdB%ofr~DIs-Tt31XS%6)ry;rrihWChO7emHm5p z@dw|>W@cQP28>&!8a>kWJ|gYTlWkWqv2fE5b}rfWa^ru*K$$@T`s-i zF_Y)dmBUi!w#+doj+G=a`LiKZNjc8eg7hI`yl3^v$LSt8BY6N9HQHUF|Lt38^;S`m zdS8qY)wpi;xOS&*INb5m(9m%4kxH#1CJCR6i3k;hzPlLpsv5P8t;XlE6>#UPRgMF;xGNu z^QFsflj%4#K0>bwiyyt|rl&ZA*~uq0qi?_Irqy9?Z(ptkE53?!T9n3PZ+zn!?J0h;TQt4y!$0*yZBIWrJvFUWzdY`ru-1+Z`RHd=Rm2cD z_~?U_D;Mu-?{ul=4^ck#2u1>Ez-Yu8gT#6P1t|kV>r%C7h@_*Tp`l@{O%dV9xjZ7G zh*4An!575kWx6|0yNAg6N$~7_=mB~wOE_c5o#pVr<*0mHNOAQLLV6D^sl%lhM1`fQ z==xUYt2cb?V`nUW4yWLA{m4K4r@gYRcUD1s^tw*J_Gql_$YzWg@7Mv#`6Zsa_nwm- zLIVKGf&0IYF3%$|ATcCk9+axAKy`h4+lGdQhQpAxf>Uu)ZX6{w*@`jJYPETYb`F8EIswaMR{?YHp2lt_XIY0 z(1@To5kL`i-NI=tCJH8|%E(5?y`iBY)klx|I;4h8DMwC;TH%WdD?*+brn8KE&bE@y51JE*veAMhVKQVVv7!^S1A7-Ts50`S8cjxERYR z8T1=&`lydp}N+Q&qHIu=vn}9DL@C)zx1#EI1mhWfY4b zp^k@%V6DYEOK28K4Gj$^$=EgtOifKu4*O(ThA&ErUXOTSAB#_4Ty3!ZV8Fib-buMI zk7yusN8d0d>V#v71Pw$<`C}%_@iEdL`rb+vzQad8(m&JVJSBtvzIWWXdsxXm0ef6= z5)m1v?I)>N*6h}y_skAaBvK6ebUGc1qQE&v+gXamgOtxa&c#&t+S|LAm8YJ-22dX{M)AH} zXa7>^U%Fv*yR|kghAK}~#pctjDhDrr-8GMM7AD|R3$niNeS^+r+wbgq4e>PGwg$zN zJr3OW1N=qk!Z}rRIDnl`6MMZ{D5jyIq2c67hG(r&U(dJ~hqaVt2|?NFEW@WCK`)Xv z*fDyZz4Mz$uSaf@mP(ej2`{mriFI;Z2!X+BpQ-5?3LmIMLTCH-dv1L1$IgoO`6*{j zz2U9zx;J)acc>h%aP6gu)sWTapJCSnKRlg3b5Y?J=Lz#KP!)YJab0tq!}s6E+J}$w z-5VMjE@F=8=dfO2##u{MRcH{htc_TustWv`UHIkYb2-ut$=aNBJ zCCf5Ym1;1gUG`ykaY%RXf8c(~y}M8iR7Ht38LD7xPF(Ze8I`(JSO{VW0>1L}x_u-n z;&Lr>`=m^5J!_?1r)jitSt7%m^U;utYH!dwnubq3L)T3hGdSS3WNZ8ZjrKN`T6J0`B=|2gT+Oj z{?cdZ?%#vUQnglQbBxUxRz-S{#PQDD)^%&ag15=_KDT8K5ev3ec1`%|&Hw(t*n76d zdpd>6`k`O^<@+M`nN$7?67jwybC$t#J9yy-cVE;x!6!sj(q5X!pbV-3QBJ}^GB)O6 zax@0Mq2Z$CrTTaaK!|k`g>1nFLz$B_~I@G&+nww&M19BZZkxLDk_Lw z7tS0Bc?s!#4Mq`bICSt3nRWOu9K8MgKk`F(&slRtpO*5-4Zr)lFGkhd%qeE;DUi2w z%3{Ejft}y@CcVWoDx7n{QO+M?<>{vh>Y1LK9k)L`Tmf#hQy=fx4>R1Nf;9b88yYTh z;!AD{Mj9f4h!~5>EWYwM47R@ldf}GFM(^4E{X6M|5@RESqK_(spr8hqw^?gK*T&Fe z-CsV-GUjGxsRpGA`NMDikvBbfHphIrQesg%v-b|I?FKR9)!wM6KBe)GN+Dw1nI;{g zsl!h|2^!H*!O}tYJ$sSL3Z5W?JufgVC2gD2?+zFVK{PZpG@LwXeokxqT3KYY(2q&k zQ>;OK9P3($5tK-^ya4{fHR#>>Lo7Y~5Ly=W`+a7&Zb6OWy(h~uhW$R{(mk~(LKFjP zF+{AwaM&k`;-))&-|2k!s=*~^b;o+z+MqB0$+tc{$U5Js@~rAsL$b_bL=i6-ETU33 zb&semiPnRR47ww#!?truDe)`w?D@_&8|Ao{pxpBmet9R9fsUERhPwZJq{j#JZr)eF_W>8V*N4YeS>K$y6BqeOy$t}T;LgJ2m9N)S)m1oq$k zZC3a0Io)Ahn8ZlAZx_DoVO5}1PD#FXopEnyXt-z@&jYEzb?pXVQt~>vZ!J3LQY>AV zrEAf9_TF_DRd;n=oS>!V4XQmzw?HMhK93;R)PNV{6-JstgyIq2lJ2oFiDk53Lo2Oj);*5k-C`;Kt$@0 zaFH@Nxcv#!K*Mz&}-{{9~?%v^f;oxX}5E7&|Es7EEH+B{_(W6;!C zZnCP0LS`Mt7)oCtMmY5F{qzX&_|Qs%{4qIv=WA1jc6XnI3Z0@o;ho1W;z5Q{i&vzq0?{s->ig}d%*Y}H}taDXj~ zu~vISLqo%c;Um@HjQ1DWaF%xMWZ|AWk!naV5X@wUE;=!pNu6_4U8F?u7y_aZH37w9 z^69cN`IkTZKmAV!&c)czSP*sl)vtXjw6nV~HhAxGxmhPGn2fpub)X~B(FiehqLG+( zoEj_Q>H^Q-b{l24(RW|V>Y+n8F<2}ygvRCG(9p19NQlH@xY5HnGvvYcRQEi%ZeDQaXEyIf{7LzL880>(d{yA z;HlfcOdMW#E5K953V~`-k>cZ*2;fsQ2zIGcc&pt({hE&4> ziAhsMMeBHZ+Gv~lc3W2k#M;iONgGug9ne~JvooK0$M5`i=PV)YjBfQl_MiXCZq29f zQEdgSMF~QEf=0nbu+gUW?^@sP45hD<@P7m?73(aO7A!yWI6HpWbYTNPmAJZqix7Pv zsuG)SX$=hx8%0#qUpQgE)&2X}`OPoWv5_j4xLmLrv6|-DQ8&vGCW12#t&*3*^TRVQG({ZL3u@fAQo0kY-wCug44D{Vuc4rIxY5qu8;GIiji6 z>{vL4RF{r{s8m%&jFIW-S;$%}toB~;?b&-d$F*8#+@Rn52Y;}zoMoS6y0d6q4%Eim zKs2ceeO*%!6US$qG6~b65-@mVI$`qpNL z-q6snai}~0P5Pl{pQ70D6y4<|=4Q6gacz3512C3oNTt_O%evND?jyxGya)*1p zZznc$_@X4UIjyXXs3jOvE5)&9<2j)S9-C)mSw{3q>Er$_mtOWIZn|kh+R&Xfg_4{9 z<$u_-7{YDAxHTtc>y=HCupg=DO&Y_;ac7&6Jgm!e2K_E9FR<&o-zJn7=PteMb!j^= z&Jt9~@)lXv-soX9G&EcwtZl$3YmRiE9NDB%B4W>Pk*huT{(uAD{T8`3M57G+kSdf| z5n>4Asw-g4#&bkOsSp#0F%rZ^Gd=giOWt)P3S<9uzj$R(kfmoMH!iHPeU=}z<0jIo<}cE^*E!E!LUbVBGDO& zqC%SXrwt7ar-4XBB#zsHswB7aNHoR9r0%I0yb*k@DcT-VzyX^kjR|!k2 z_%h;6Ks$nW9*reA(DIOZmxN-}A!9xF2;wnPVkHpDpptK0E~mEs)kl8+_YZB|Jgf*iWT}W0_e4U^;laVT4Ces2trOO7v;ZuC93b=%k9{gIEX zY~*>Jb)W6WKKk1`o>^Y_bUrmxbvhjumJiaN%4tvKl!Gd*d|(_q7r;aryEbB_VvS!OIn zQQ*C&WevHn*z?_QvG>tO&-&0e6;*{G^@0afm3V=5I%;TWIQK|A(hwxqI;7ktMbNsG zcygF5$F$GC7<$?3vHz|+k(HGsUn_M2U?jEi9*LWgjv$6$<9NqDMh*B(ho$N2+|;K( z^xpQjjq6IcwACV;nK?Iu zJ42`{yS{%HyYIb=cFVC=EXL?$BPn$WmafSiST8J-V40M$iGtUNbva~02qlh4N0h}M-ouLz+z%H? zgOXTQL|{nLx%pztf@AhRoFQbD6}@C4yp*_6(ibEgL%SlO^`L?ELDj z%!)xZ>7pD_syFU&p31AES?`&!F^&`Ee0!?jb=e&^|D*ruz=oaF8qQhW)BAt=V-GKB zbq6A9LA!t|Ufll=Nef9bKp=C@#4!%7un(#xATG3r|P zPahFYa)|^x5z5!vsGD-gq3_>C_afM(k2?GK7|8N0H3bOq-1g>cXlS@#P{x~5fN{bt zOv0en!f-ewt9Rm+4@jOf6oa|svU5J>Qzm$JeDyZU9Xn9YDq6 zk~@p5v-_v=7G>cCT%$i6+(N8ZLfO$IUVci@WTD~v3S>AXf?z-VRq{l_U+$)+~i zJfj>IfI%}yY1)g~_RIe47yk9XK6v&=f8v~T?a+_>_V4Z*v|4wp6jdeG)bWUP-)Ofv zGT1O#F|H-8pbSDJ_7>TD_g!@7H`V)k#)(l8XAoluK2VhvE<3*)BQ!KLY#{aIIx^}h zA#CI^6;z-oit$FmwOUx0GYEmIHG|B|ZP36^j&e9)*Vn&Fbzm>P91=oOD!uHoD*;Jm zYNKR8(fUZqfW!WP+1Xiot6i+Mm^^0?RCRXhiK}mT_cuv>RW}XiUJU$(8~WMym;K4i zWm~@`MndiA0VeMB(VJ;Bj@>|$o&4b|$cXNLo`*m8X{x~mEH;x_f=)6rf$eXP1T?9#S^YH*Rj@60G_?QHKq6Fsm?_>8JxAWxJzrGQtb56)-W(g{&f@BVt zwZ=Vo8yXrKPL`zqv8GZ+4PZg-xTTsgCRNusORqn`<#}AkusT>pj3JhREEsG^8SA4ips9z*L|7-!b9%iVtxlUkF(kHfzY?pjU;2hO ze0ihK?PzS&7B9Z%r*HhaU>}4!%aBwip>+vCtmXAaMtoH4FXJ~q4ZYNY?U&9{y|9bt z{_a+a)$RtJ(%B_zPo=E(I?G**5MvWVZ)j*ZJ=Pj^S=W>W%Cf}NVZP4nGXobt*lh+hNl6Mhx=od&##zde_z)mf^b!=n|);Wr@ zB5$|o%*^=%tIK!X{Hq_no75I`)3I?u*c)!Bu6WxAZ!Kc}!`xb}j8bVJr-k*B$^){D zAZA^Spix1z?$TxYc8;M|>xx*7<9-Lp;GWt6Quj`f$?NM#A+WLFM7&di1XL=V2AoDhrIb-IQ*HX*lXY@huXqh) zn|m`+^?U5N{Y&WHJrFzu!IF_hi+B(x1QXXRWISq0qA9`nlo%Ksc#E@bP^&A|VDFa8 zU-ro>e)FGg+GMQ_&8oWTU;K;3W#T7>uona;8C6x0Wi6&!Q}{BDi~bL91QcYP2agp8 z(W9ZjuFmt&=RZv~+!Vq+XO8w1`PR#lT9Ft;1Tp82s6)f4G1@CcY%Rr^JXx`h@d=8F zr*_C%6$6<}Zb=2S&VXL?Q(>LAAdz}698P3?!qr$x^zh>hzE=$=M`}o7kZ8tz&PSj3 z=yS8iV8qgyp1~{hI&;ju@ogJ6<`W`Dp1b2KEPel5m@pVC+)Y&O#t$4{gF0Gq)=0f< z6|wY(1(nT*J?H+zws*YZyXX3JPKJ$6llJfY<=^;1G1K|0a!?g5Quj5lzE*>)CzCZ# zh@xwR@$NbJ6Jo&j#a`3rl@FaUNO013o-mDGfrk@c<)C?7qy3c94)i{ec zE|&S!L)X6hNB{We|HnU^->CCC5jOnm^uho3zdg9^Rj>T_xT!~T$rKUFSQ0{oNkx&s z17yeQ|nYBUOmw? zdz|x`V3lA){d4UO0cJC~!yx+fJTYm!u>S(5{h z#yC2XJAd+8QS^wTVdF{jYWfVvdLRNpMqwG)HakNu8FAZHZ2iE8H?`br(Ou!$FWy3L z=W|R?PZR5f@+ia;*6zs1-=RsXRHCbIPyVHDzmJ`rTI_aa{_JP}`Tu!*C7KOp!wovX z&;A$xc#qpUefvN|KgNi24r4~C8USfYr6Ozk^d>h-s3ItILF)PxJKc zUxBbGa~Dn;>9ojRb}hCwg@y_>5#u(+$JcO*L<7MDybbj(J>5P^QnN7~E*e}kh-h3F(k%$1!)QTON;g>Y`u+*dX;! z#^++|8~{@{yEwsox`wk#`rK>~qNHnaWE#Bp31BmxFvwjg8Kvj$&9DtH8pj;9tfoE~8+rqpBikZv6~TeD$jv zd1hx0v6!o0Nxw|OM8<$Lv*CG8oOqn--$#sgOUdm%$%Hqfu2=OAV&v97l_MIdU*D`r zc1ywSi8mk}y`R5@#mAo{x0ZJ1#)f&)(|kfQ>h<}kS=Z}b6cD4{1=8*?cS_qqZY!V;{jxro2(^t;;gJ1}sAKfkAf_ zVxS5`y1QTCxi9@4gM$}_EbP>k*AQ@6ajE1Y1~yS&fQD1xSi96vmvK&{DAUdj%6dd^8oyeuFV(L)V zdR#U&qUC z{%zd!>;{kaSVSJb_47RO7k_~tcF8)H(ieE|lkFILl@dlLshgu~T?hysjIVd~1~2W$ zbZ*f0MD4`INI-}x#p+;i7PpWbO>+f&FjuR>iLCmGJ#&Gi8`oD>4M0M6Sw z`EAnEpmIv-6rV$x!@!n=JP`6g%%NL#Arr(1rcOXR*~12KUdX)QyrkHC9jCWyv1-X| z3mXjTK|QtZb*+7^k%-4QA*fIF@>U625uFlEB-BS`gvtos8iKV1XYfs8+=e0&v7&^k zB$$ZFEchg~?JPt*-c94)`X1cYOE!AA$7E&Ci#&h(ZP>C$w1LV5f<$cYQhrDMxOIIn z>KqP2Qr?W})7D)B1XQH$3zvQISAOlcHchYd6Je9tgWm}M@PGJUzue8UPgJUfvu#u? zV$*Zh^<=_IVwZ8~S#?g%3&xgC;hvg9Oi#k!%P*oKglH|?dQNvL!wi1tT)iq*N z*F?(eNg+mJjFiJZs!E<2TDip#(O7{-EQ*a0e1$_(lsziWTC4^{Cyl7Awb;yo7^)B{ zV?c~;%GNd*Rb^&w3N+F$`hY>B5JI5DVBYa=F8|2S!X|CiTn+l%`)B_T-6tMRp(Fxj z40sc%!umv06W9CAZ4}y>!#aZ};Kid=q_btNS}MYwum0eV|LGOK`J0ekN^ ziu1;Ma#Y%{d>yUp-$A!Xy2K6VH?b}o8kG(Sqpa|Vi;+vFXPCAvV!wo*XO=cu0r^0% zRZ5Kuu6D&cU3bKcqt(ie8WCE11df z#y~WzE*4K_uYUDE|Iq*PzwFxJQ#@%l4P@34 z#S-JB;2@YJk}!%1j$(JdD4EGx_}(hd-1Y@_-Fr9a`I|`QW@ic4y^i*_%RpScr)oGK z83}uYvBrXqcj_*rqqne3=ofTMi;nAHLxy-q=B5zM5wYXC?m#3EQ{r6Rknp7rjG#`4 zRtN?vQ38{MI5mRTh**PbS;Q$J4yno^>dWM|R}1Pek|Shj0BR{&D~`-qGAtS+q4Jmr zd6tnGON_B8TiZZXAnUZ~_j(wFTr7i7(MMQy)5PoF%k1mlw9&&nCi|a#id|p&0=BzC zf7nNz(r)Le)u}P~a*{&5E*HzxDvJ`CZs+y428I!)X17EYHmz2j)|9X#K+8C)UO}rf z1-|6*PyQ8imtW2$uYJP?p5Ezk=?(AZxvzWzS=md+v^RP<4W~hA=d%$dG zhN`MCk`aTYKPYHV%|WZfuvjARI82>UZcO@PVhv&(lBF1;%CW7ciV`bNi~)@?B@o4k zN-B{Hg~v$B07e{n%i#owfud51Xb7$a#vv%t1V9lHoY7>ojUktMaS&tOk3p%*3ha@x zu?^>lXdrhOo71w6e!oX-<=B~7T5tO?uK4-iLN=*p`(S06d;j>~(0|}Q!muQ`NS5UU z@6pneWvx0#V{HqX$pTV^I&UCi0t9bRZHM`u+;`2pZupz)Zu~FK%XsEyHR#;<+rK@0 z?C(DJsd9eLd)%OWyKCiQj4-S`#u`){Mg)SUp6TQGh>nG404$EYm9si1P^&CH{16ZP z^(XkzfBesA&2COt(($l!Taj14k=9@V?F}}1I1Q&rGDzIeBoS$Hb@JiVq=9NCEtWvr#G53Il2H+CcVGunOXXk zr)L#&$)#NXE5FWLe*Twm?al_DgkG1pyg)gB5SA9vrGwaEmoV(Zu!mJeVuh9kQB!Ft&9%FM2q7rWIV6_J+Fkhy z+8_H(wtV0tn^HtZReAF3U*Xxm_+z?HKY<3QR55LbSdc1`8;kRXP*%ju5ycQCBq=0K zS_!2KL=&(sVk(PWnJVpNulT=y;Wuvnea^E)xV3EB9XbFv-Z=cl!Qjh({eS%P&lZb| zztd83BMvc!5IsJGj&`9?<6BInbhlyWlhD#sTR0iTi=D=>iS9Y|^cNx}Y9L-6dzv8u%UxEkk!B_a+D#OKjibH$Q#Y2RJ{S5cLK)EoF zbQf`hAreP5&?#ItX%}*oi*uY7FKcen!y3S??-;f|Rp{`=&-(Ady3e{^Sgm{6BVT5N zsWtOQF`6~XXux>j>t_cYpN|U$gHRX)Teq;~XAfG<3GD@lT~g{hAFu#Z$s%Ouzj@tbYCPaEpg9WO(nHo@x<_A$i@gt};r%5=?8{ z!3$EIri$X&EECg4AY;LlxK?ABfYD^=ZLLi{#>CV!dVIr3GBZsQhB#J6O9mZ`g{tz1 z5wxz0HDU=G5CU3x0y$Gt(-d}!YU^c8zxnNKeaCz0y!@58O`2tVOsq>stBsqUW#;mi zG4uMjK#YXpfH3ScSUAXV_Y0J}cOd&-z%T5hTw1{SA?@f1WlFxwGK&#GM~1Z);dU+~ zL_r*=R;Wg-F(`o;JQxsX@tUr4Y6;zuZaHc}0@3)f;+}!^ArFmpyV`mYEXqV0we|#_ z=6B~Xc;A%qGmDU_25m>4l?AafKN@YZ0&p+$+aLS|hp zI#Xft7E(D}XUp<_@Bia3fBH|)^SWlR|MUO+KX3FHj>`?Ev+sKk-v3-4^&_DwXAO#R zkhdHf6h!K*MG!TqmNsprKs1%Kxmde51*<~lA#}T}tS)oqTi#06ncC2EJQZ4VTj}n6 zp6$IwdgYK#r$a0%h$YTKh(3vUhyY1!M=(H2`5fzlZQ`iQ*+r01WrR#>v1P0tl@^EV zCebo7O9$CB1pcr6`;3Q?zgJP{i4EPW*Qtw%6FNjS) zurVb`rSiOVB^WeGevX!FB2ahd8a-xoz1VnWzBh-f=#Ag1WBhG7$}cj0a2jmNn=l|2 zJjtalNyDH&?5FJZR*o~4=moZ3M*NW<5E24)k7ARqhE-|@m1K7$*qf>BCeB9b8^ zujI)8AVRDgd6hoU&N7@x3Kyxz<~HS+AhcRD<-V2j%WuBvr~dV;KKS94jXuZI<-9QH zTz`Fd*QHnP`R?7{xi0g(IX7g+TDq$%WG+i3HY4rmy4l6Vk8FWZtHKVEg(FzfV z;xOtk(UKX%a5y9wkDJOdnIX>{SrsXZNN?*Um=F8}*ZlmiV>fG(uqp?heu@YF_}{Yp z;?qPT2El~1pjN514Qi8~yeWbRH~*!HHkvHO*8BwJk~7CqW7!x$J;0nKuBol7pcg@uR- zX3`({{5s_`OFeT6X~Nbxaj}l()XDo(@7$xQMOczDRdNOi#F1WfZHyxX0##Yjnd(q# zz_(h=ml1c}8`$!W4>5K9yP3M?T4;C9yKC>PU@UIyC2W81kHNd%Pqn;AZ^u)Z=bxtg z=>5d~&y#Clsk@4afhlY%NKz%#f;HA!tT-SNf{qO`^><%Xlk90!R2(*r`d`bqKloat z4!exPDM|mlzE9V5>^jG&N$&M1fT{a=lciF{x+9-45$6oSMbtzpUqZ}qmEbP9lFpC) zG*{j9izz>2lQO?+Cy)I3zhn8S$FP}_WjW=_kapgpQa@gDLme8ay8uQaP6Uypr4S_I zLqvkmGE+=#*}CF0`NRM6*Z;xYoF~m!j>qQCaz7IDsbBv1&-4%M`{Nluyxgdl7=03u z85wly%EM^ZH7^M=iFsKC7wgBjGX_<_U3o1(`M>@vE`7rrH|ESvgWis(*!L&@8~F=6 zXluX_83sX3i*Z7bR3IKC#cgw+x?Op5aW}}2I*Djhs%_Ma)!W7ig=6aYI6Cf`>}{G* zO~@E0wqa^lePDVzV_{)Gt?3q3XBK_UyJ)}jgIsdM2eEUTcM{!vL}Ir~T%2d=;k#M+ z!S~oUT%g>&i`*K5Dn;c%1lP_{h2d~O%VukgZ#*J6ZGubg*)^?TCvqf{=hfPild+g_ zI^7Wok0*6jhduSAV)7;urAi~!3}Tjk*5L!Bz*CXs5g&ajR?v*xO;OEV#gLDC%@^lG=!lT9TT2gPdZdDx=tWa_dB+lWgMJ#W(uDsDMds zzI1`bah7|(EXcQQCtmdm_>rIB@}K+9+4AOhVOkeZZ2X)cNZ!J1*~ZkX-$ds%*VAp! z5%F|a7Z`ev%Uih25eiSJDl+TRf=zT1J{fo3vgqOY`t|3mB4fqAA1KKf9gra<=5oFD6X!kka|A!b&K7UFc^Hm+78jQ371^M+{b^Nx%Yk$ahq2@ zx*QC6>Xy&4?<-##htij59XGU5#(fb|rAVqCNwURd@~()%W-Th8 z=}@yfE_>yx{?)tw;1Bj~@M)e=&NqY38{T{4aN&W69~kskUg5jT*E?fmw8zv@Q;Ia( zjk)`y=8CHsJn;mwcnCM@ zlO98g4N6Qk&YI$X2zaA-lTv{$)}**-G_@o()QNCX@2sVckJpd0sZX++bZx1Oe%4wC zj21^a_JSBGQJ0~wcq97WpXQ2R{eQ9bT|b7K-Lipae1RZ>+p?9}*S?YIcm61?cYctt z?NVY?baeo|f*hD@w+K~9{mGy*iZf?;>j*}YYsN&PNmUJ_owtar$*TbBMQ|{RH=06x zW^Jy;DV~b1KThtsf{TKQE@@5sykV~()g5gS!SSeYS;X^>JA`-CC)=iGmw>O%I8c(#55Gy(^ z$Eu|opk2?4uX)dV|A(9Y^iLn$=+ivYoOcEt;MG6%Q>(ZCr$5w|k}9h1QF}k#ED)letRT zxkZ7HJBq2T_}9LjOMd=$xa>!NGQ}vH82On+1j%ypt(P$Kn(J||cpbeerk)Nl(Ce*^ zJGyD@K}m+MwtEsW1XQZ@`PIp_g4D5c9i>?TqwH~+R5TzP`u$md{nZNs33YfNX>x>0 z5YwidxfI^^KCbws-==l#b({WLoPX*mp8Ze%k?u23(e5|`(C-guWv#Sui;A^|XsS9H zb=20^0-Ev*B$XqC7;tKc!Juw-s9Uyv>YX3?sXyQP_P1YfvPS2nK?ivKD=&ZXp}W7= zqhG$Q?XsDwifGj2aw|Pn>g-pYxZp=FRIOcW(L}H&l@qmdoN;KcVE*}?gqbNWd-ZEK z{e?fq7}{4~OK+8a$bC5DeOxiUZgF=Bs|>{r}&$|C4`mLAlyToVNyj>g8`Jvufq>J3X*2ZkA#wYRD|%WrxA_?=T#-HhM@v!0JO)l*enUFUS2 z^PK1TJS@rbi(?041 zM0DV;Pad*JiEOnaM|wddB$1SOYfw@Iags750^T`< zP~hFL7iZJ~A1YnL3%^JB@Nu?k+LYB&kPzYfsv2pWK}Q0N!@6KlGT+OYSaAgX?kYIv zsZ9UcE9q`L^?+V4ubJ6;H`jjnU2MJb%S7Jbyd`KoXh1IZJ*ZqWN17v}z9~`lv7}K< zSvs7G%^d3c#meRXMpbh8%N1XA*?1$gDY;!hOp4nem*syGBN|afS za2}}$GwPKpL>x+$(MwbxB>p2{y-RraHJgz_YjMk8>!K^aX# zO6*&d7F-gOuR8{P{!2LG`7a|q>I6iTaJXM69SF!cAzHbH$y3gxbMl#3V#o}3W`WWh zob!mx(k3A$g0fNubONOn2!R!D*eDlD^(cwDufFNC8NX}on5 z(lM}*wx{r|N#<4@#j4-@9hRT>EcCL2ZBdI-a!bD z&9fk4VL;0i=Oa8VN||Cd9EU&msjPd^Z!&$_f{@ zE-Q(>LZUIH!+A-hbnwzHEu}3Gs`8tcVG^Rw)@7x*_(%uzI+%MGs)^xq88UQks)=MtH&$C z(nrq=i~@B)u(L(K8D{3pX+vdm*i$V7r%u^vu;JQFW?DXY(WCVFalvrc{xAasP9Tt@4JlW3oOCPjCe z`T2Q@evi_Cmm22?$A}cP+bv3KM-vVNU4pu)&OHplTrBx|z~e&ToyQ6fGKl8XwzCdF zsT6T%8D+Yhs^$4?#I{?DMOH!LV=gn1(gqu zebi0OqG}41p&?aO=QSZJJ8gxJ+U0_dKezmh@?v@<$XQEA*!zAaL9!Jj=ERtlB-ghqBNwfR&aAO1+7*>Z)PX6TW+UJ zJFGo%BjP|F^%x_bTt@r&jr3+_nZIu<-WMn(@UBEw7dRTtAtI3UbK@$>U{5U#48r>$ z^XbA;jWyt+h-mH6*wY?Qdj4}*`ON3iI`YWFy25erA`o#*=jdbUocdVw@h6gZS~vvK z8l>}-MTRX)q!N{^G-Gq$a@Yy~i@%eHc)SYeUX1+Xt+iNdDTT(aT!%dEY*sw~<*fLn zXOXU6`%}E?frBjzw%u?ox4i#d^zOccB#w}pO2XF#d27O0Xw~u-GW6Qys1ky*(pS0w zp)W={g%`>#IIfvE`lvs@=#q~d64I^@#UT;U0nYf97tDO?(tp}2bNkG0q^A4XylAyr zl->`g&qsN>ugo|EN(8x2B^1tv`}$cQqZ~5zXbG;rz|59=XsthrW$TX`e#j1Jgg{M9 z(m8P>Wj~|Xa!cjF=8?jqO|X#?KPfdhOsHi+!#utZPJPa!Dn&t4lqB5_*~-<(r#y@G zFL)W<<4ozd4@pQyd$I>q|RMsah3|fMyq@-olAQwiXJkp7W<*@l_ z0y+?+g$Z)i07=p!-gp}A=Ul|fr~N9e4adPj=0yRp{l@FL`CV^k?yg(N%K}>zq*9ah zGg@g2gTu<&bj8;lZ`E{4SX*~OU{n|_4Jx2vjLm7?8%$39^($ZYsxNTLDfXe>%b~y_ z6VL(9KIyT$uK(VZJNjAwQ4?u<^&smBAr;E|p?H5_q}lAOCCsd=V8N&qNh+XbkvoGcE#8Z&+gdCM<_}UZuRj07+X^R_99Kg@nA!y~Bo;E%_(2Gah_G5?Csz(-wJA63G~W3T6f}2>K%(-j--7P+nu5XHYu4b8Nly zcGf(4Bi&W2_xINR)QC8ybIhZ$E7nnFmVC!nQe%-KC}g1&rE@qyB{9Z(N@Gz`H0B*C z7>f7Q!IHXDJZN1D;f8`8;Q?FO7~1N=5KFNDw$(ejnFA9WJxX^+FNKY_9{g*6`2%V}$kDT*LUBNWaHTxAd=1Slzk z{sk8A3{naNuBsLL;S$f#K}vb751#B^cwA}l))FI0L`0+#`h`aybpq!2Q|MmsVwOGa zxwMWy71`<>G>OGhiru@p_Fw*i?N@yhXA4jQ;cSrDb+$g`RN4^18$mKU{%U`EsWnAe zA_z9I-a#45d_Gc_xz>um{?&K<)8_rQk^6yz&79?@;>!2GKfUq8|M>q-n)1z)LU!Ds zhb_vGeYd;h&eO|ty4^__l%eZgxNxGEVZKrNu$JnrM<|VqTFhoSVtR_sagXAe|LrX- zJL=f|y{SJl<@_8wKKVY})nA9XJwOmh(JHFuZw20alu}r0_i7eV8%l)7{4l65#ml|B zxd|+IRiER+dYE7AaOULVAKKfeQ~)x}B@VCDD^~^}cvd@O!G)z08EH^pT!|GRG#DT3 zW+@bin7(Y&J^cwxKj$U1jy(?4!B@s?c$jexGqa0@J8oh2s;}cW-$Ar6L%A?Zs)O%? zcO}jlaA8?XX%QN`eBGVy@jjTy)Lv~CkCYOje62){lp2A=$%rzUq_gp{v`#&pc;gww zD^?$rW%Z@Z-M5wN-~D!Wee){}dRcX^8=BvYSKfrzBNH4^XexI{gp?Y`^4A0;I03B*FM^$1BOE~paZ<;wJ$u?-?i;+t=YM!cOxl95=yo6^8?!L zF1hu13z2|&{3Oh$j|X&!MMR#LM6EVG>*yIzYxN4&obx2kdDX>C9l2qDZ|cv?VA~dE zuKF4?U;hm4{vLEupuNWC1*wW~&Jo24&JE2$0ft)ER)Vdv;V`#5?o2g0AnQOr9?)z1 zTIp-cN{F**F%gPmf2o*O>$0q*?bC+KhA zMl7N+b;hMhL4BbwhwBrI0=jg#(h{o}ti!3u%_;F?J2CY?UjOHR`$^6^tK5g1c0h2* z26RAt^i?lvGOA5aC_-OfU4A>g!5<_kr$S@ z+otaXx%b$XV)~S`xZo{+L3`Qq{kx?<7mgj@`5OH%Y(n3A8>tKdy{#hp3q7<{P@Rmz z#ZJhQt*mdW5wF^^*2(HZNP(~juka#d;!cDdmwu4DFZ&p~@4uH;OA}c=mH>p~-ia~wx0+$Y zfy32a$HLJ{5_hX`?FvB0zc^_a)@&%2XwBvJ+i!VJk_e4=b@Kbk-f>s!Z2w&O@oC$01rLo8~VxcUtc)IJ?vHD5pbJnlF zf{AtO_V?!g%=ofk{^o00xcuWR+;tPh+#Hjwn6^-8=g0?rln(QpMtNMN*rcpL`A{G$ zHCX8?a|8#-u!K?N+)l{qKn?#ERc5ZN9F^Rw+mLCyB{jE^2elW$aAH`N7ZUH|a00-G z3koWyIV2866eSd;!P;uQUU-xeh$zCZ-avH5(`cP>Hr?ZoJ7lt;hJBAKa?1WZ+1xzE z{2pw7E=f_w|g-^5Rj$4SNAk_+! zhshEv{jj7yYQh~|zRS^3&oEXCN0}G2(zc)Xde%;_eAjRN&c%PlvtGP=-)`N3!yz5e z0j~Js>yjJ)@_R4c(9v%Zz1~sQSQ$%&DJ+RfvBFR{lkP2`$3jsIGTNO<7WzHB66iEW zYRzn~M|Z^v^oghP^#AA2m{_%DKX2{Nhnd;MJ)i$q^c^=a*nAgeZa2%@3D_K}b#N;M zc!~58$||%~I026i7&bH0e4j@&Pb=m z?(qmQZlEObUQ!qjQAE^kQzRYS%GE4C_o+;rehzBI!K2mR&~PwfvH>@2`Y1Pj=n`yi zmPiQ_p~-p~X%tsgJTdZ6Sh7S{2Xo=wq6_?R<9wO6XUn+zj>$)z@qb?Uj(6}{KGx@IA@@*Hh_+m|I$OH`u5*jiZl8U~)wy&a~L%iPfJbjgbW zX)7g(Bj)Glz#9@RDf&Ik+-|ntcR%Y+J)OAS*}oh7b0E_;laD?HH@yx&zra+UGnk(v z>89kEP-Yj^TGmikS!1QcNrw}j;F)~|? zpE$f6b=C_f!E11Fxb-o@CrB3&*=p2`DzN)tLKA*>5DAG|8ZkAEKj}=`&v*f=pYswr z8y}18G?u0f4UZ%?&$;@%wqK%kG_td%mTwL1aba-JSC-F}8B5hp zSl=QfLZ!B+}tdh|&|t=9h9)Snxv zBU)>ZB-wBRJt4uB^cH5S11qeND_8l2J2||HmcpUEuT<4S`Ak&;E%;F82qCuahOat$ zEA_45F<#w|XQaIE!$pD{pYsi`9);qS^=VxKBgF!Gg(iZvNb7x%Fco#O$~iY>7e=X-!^~!Jd|&T~SN64#!oY3h2H{ z2;A^2P>;_Vp^J4#6NlQKY!>J4tLp04ug{W7~+x_zVNvA_WR!1Rqm+^c1A~$ z!VL`Vv_(-`q*i2wK`DiEL1aewQMo64%-pbf!%L5MR5FAbJ-vd-C;l?$zVY{HFFzO? z#zt`1;FXu`AQX#cCQzL=K{~(2~*^a$DlCNG*|);B)Lis|1A(!QMC`rHOPB3}u|hn3BBANYa$tmZ(TkxB{Uy zR(R&if@s+^tXf9C=19b8=g@xIg)BSo`AnR8Ceie&gTe>8q2W-$6a_#2{Abzx>5q`# zyBY6II8RiW^{9J&{9GD(n4A>Dkzt7=Qj#c=WcdIuCA!t6^!Qw;t@EwcTh6}l6`x;s z)>(u7v!w?fhf{_gxaOMZoqzt{&PsYS|0gl!S#2S8)*q0j!3(`N$Vgf(%Ce*^OLSb# zjt*a7)R-YX2rKZ$5_eiy=P2@$*4j11XFiFiy#CEhtUhADZ05sCk&|uP#@y{UV{iB# zX4_q;GQ;-vkm4u@8MZJe5wu#pbqFQVk;M9-K&Q1vNQW^7R~od`BuR_;o+XZ3NFN>x z0v#^M??dqd zaPwz2aoq>sNz$Li<|V>|9cJYL?HCGrh3@w7xRVm;K>4s{p7$15wrm+$QII*XQQ~u* zY<6k#53m2NH~%9ST~zGH%{|~a`~o__l~?L_{_j6|M$$Kb8WsJsBX4ExWK@=xD2h-@ zQM#Z@uxJmBbzCb8+DWw5%w-F7mvzZ{eULG}J;CJT&gJaaypfg1AHNSb?h(X$C^Pas zd+2Z9iraNB#l3e?Zo3;dGlS^OAc_*3XL#rE-iGCi(%`&7OGQ*^u=~*P@oKM6Fm&yy^&8z8bxJHOr1U0pb*rguMyNG&D5qJIZXp zO`rWFw_f@#@|{~zN}z366E{NRWUW~N+PS?3^r6b559Rn)5;3>1Ks?c9wzSac-nS=n zzklA#F8sn-7yt3>e%#yxlEX8g18mw9fAkX{d5+t??SIFvI6j*n$XIK%mgIvVMuHR= zU(Y>0FkmPLF%j066d0@u;wREb$YE;rk=RpC;g^2@4_UVE$bG(vk31e%6xjYe<;+fc z+qY8g*@NrP;T9I~y#+L&tc5|3a(*829PI_M5R}4Wv_>X6jH{>wpSBULF0wm;>~zt~ zSEHs^(p|p+m3EME)RfR08V)dQo^$;t|CL)m`w<3P?;^DxZ!KEJI3ck@4kd4u_rnZ) zJfH^;hEYHlIP!czdt#CvFzdxk-kx~xFTe1We|px%7x(wy)*iSVz5yNZmt4~Rmyi7O z%a=+1L^*M^ElacvmYG^>j4?=E7u&}Jx?CiPQj{6(i7r{0;e@BH6SVQnEDUI^U5!5D zES~y%f6U~XwflM#_dPzSIbmJ+zW#W5hs!g{EXNgD)d8>2X@ZUuL=;zU3=mvb6bMFt{aqwlpp8L$O=%onNNnwrHk>Q30($itPSnA?T0_TI zrZyfc6>~Vt#PWBY_Uz~We`o*hf7`OZw)H^guny<|SH9_@?(Ns!_?k}C`eU1CYlL?q z))7+p(1l&clA*SExD{UFg{KsbIMLXCPAnCX(#+1!5OrFJNReeF$;#!dI`>&T_BVc$ zWk(*hU$(QMp`qd7rX2M6!H3?*ZC||{vuit|G`KuR3PC$*W6IEcE{2lO0xt|!j{1=5 zQPet^LkQ^gMA59x2V!#hcbQ!A=GT7Wi`Vau?L1gG>;pQ$rWe0>`JU}}zNsV4A8BK| zl9H^KGc_?uQJ4oBg@_<+>IIa_;Y&;86%h*Q3`7bO!ywN{Wkg{tt*JFcCqIU#{MSEf zf_g(k!$HqrZjKxO^&@Qg%tx4Cn88~|f*=wSYlF*NZA>GbMEcqp(c(mqg;tf)qle0q zp%Rg)q@axuzG$^S+f(9yyy=TqT}LyX_MjYAV{1Q&Q!ct_uxsbsHFy2+8kwZ#RB&zK zJW3{DBQ!BMiOF-KND#FYecPiHC6SNNLWeOcL8Wx$@y?@#LLx&p?+m>i_p$j0SL36I z6~`P`2`e--G&CGMc<-3KcMDg&^KZE4OP|KhFQ8oTy--eot=7OrWtA*UB{ETKc=$@J zDk6!N(eD*RiN;HVqlZpBgJOVATk~bo`DD3b-CJIN*;j6$5srCCn%3ZoXa2z-^d^r! z`n@)u_;BW2-kq9ap*M%6BsV2eN?M&R)(aMTJ-qZJNi>|9^rF^04_{E5167S62%@sU z-gyf*e(0as@}J+X)|(p|8X68BX71U{wU_)K^8fr6Vz7X~p}Y$+t)5CHa^!v(s2kww ze}|J~Z()HnO~D&Xo{{K?(i&u%xCN=dGpAaA`Pz?t>c{(GGY?9dlKT>_c=;|Ga7oZLUx21-LhJzdL9CzRFeQx{L4|D(5zf4<63S4D(Dk~?&aq1qV#pX)F z*W+n|D2gz~P!t2=M1!-;8)san^9{Xf-T(cnkAMD}eY16k22F;(gbObH@SK}k_D-*XK^j4PRxDf-X^6)BEUI(VTh$Bnw#gQkA0Bq zzxY{jW^Zq$p`oGS0Ks$bk8a?G_r8-YSAK)8Zeg-K=&VQ3yxEpBLNXxIzRl-&50%enLNkF#*&4a9{{r^ zw~KL>J^joTX=htLG5Pne{nX{3rwQgi7fpu#;K+A8|G8(X9e2KUDb&hCyIV*nod`^AIt66!>arA8fjmU0?qkGq>G}G=|PZo2;LQ^0}5s8>YxSp``Aq72ztGR~*9o;V4?@ zZV%HvPL`Cuz$eNt*!UW`^2on`?WeEU1myc?iw}O9fd0^^PyfcRKYHh#Km5H(C0;2^ z(b6gwgTA4abg(X`4&r-j0P)@vN1@#Ay+^AUXD!|dgbIRad7cxg7~(eR8E0|&t6sx~ z$DWB0%~)GQL&Lsj+mC+8ZC|>a`@Z}I*tLsDdOX2@!AVeYLXj85l_Moy2LqNRs*b~8 zUIp}8NCv`GkZ-Ac-pL!%_3Qum3mEFGLg2e@Zs}_C?1o{P@=YF%gRw=5q@j ztz)7n!dtLqJ!dIExXMBljI~2yg%Km!ST8_EnqrXARxQfXpwc$xh&7z~rZ;oKFF(DJ zJ#A=c*r&{H-NLv3*Pk(e?GK2G66q~Y)?>L2FGoE>0*1m{eoU_&Dv4KTi6ezAL(*-G zq221TFvy4}CRyzxeG>ec_ud&4wC&W}1Nh(<3*%?xG`mH{JDnu`sWVRWXqb za*;-DBnl-YLi);fHGFt=9O1mLN73t-8Oj?1l+GehOe8I`UKTR-Bqm$Af|D-#bsqJs zXVaNpxlcE=p`qdM!x_V^|Na$jzU*U^H{FEn4Tk2DM#@Sw3%m#!cNonb1#<`r?XqeS z7T)2@f;iEXWr>O+;#P6p&#vLDv&#Lk&4(IIK>r!}*l#_5 z!`y9m|HX9My|h0+A8VJOg~pZ!XG@|eB93Fcw^;8+BW!hlh?AoRIuA-I21SmJ(olxC zf#(t6-HTqxoG~w8ae#4Tn0;T5kTvm$>PD?;yM9PEsKd4!i|?7|#uI z*A|4wi7=|BTs4YTpVeyl*2?}=N{RCZFGQ%N$0@x$^R4A8f3ydF|939`=9g&{;2t)b zfc~>1HvRTR$Lzf8mOtt^|00$9m1&#`;XTe6lovQ}u-1kvSt*AB-4Qg6VI)4IwWOEj zcp1@Zb#S@EWd)rm0wr-15#7_z;Dld$1xKIt1cYqX)f*Zb9xgmH_uk7*U%HHYzxrk7 zZoi4?Zkzu6JW&)O(0~r-t*X}Ug&Q+hd0;2I@S_Y~N=ZM@a9X1}9rhG?rY4quJ)UU& z-EaQu8@~z9ZyMkpMw)>BbMmp5{_472ySBc%YyBVk{$R>EFIrI)j0uVyAp~)fP?$0V z^d&zpyhP&g-Xf!zA~y)3iKNDRj}(rg7=UodC}wiw$sF^-S8(#Po(o#a=T1~*_^JUe{0fIi z5(+f)f~;(HzSdK%xBT(tU%HlN&HQ1f3FyB7S6}?P_BETYdrf5e|0QMbl&F(>4SeToo(`sYAAvZZFfl_L(4RBO$lL0pz*&W%R zIzWO@ul)WMU%$ik_w=1~XD7(C zEJ`5-p#$DQJ$&g!EvOJ=VCxYMVF~U;u|%Z;FD(Ppr^x#V;W2qh+G=AgWO>Qlwk^!; zyq^^(Jc_hC*>u4-G&KB76!Y`kaM>m{Uv?>e+ZM`&1(Xm7r-Ja669@yHRtskfoU|D2 zuqw#C2@l#Sq^}(&>)2eaqzhLGy9zSnFe>i*PU{orJm!KVL_9jKMbrp>nwN2=f1o-%b1N!evGi?@;H{#_=3`w=vIt1AcYE6tHzT? z3Dy|A_Xy>2I0PQ4Gvf!xjB-8(yH&dd`ZbB4HeWM3cw5U$q?x9TZpz$P#m1_0kH2TqbU)nvvc zXAw}8jhf9hCYsbSlgX!Ui19txyQ^q?$r&r-e24Owk9U>NJ`|+r97IojBgG2GuM};2 zSN4Z-@8ql39kj(v4cJ~F%?=#NtB3NA9bHCSlByRf#_5Pyfi_qOsCsNqMR4L$sji|3WXmWN|&b@|0ztKILr^V*Ye z=Rc*5OA1m*gROzX-L^dRA*s6F5Ca@<1iAHZ!z(L16P)*t8FhYAV{75Em^PSxQY?$u$j$ZZ=TjHhj?8?rVcCsHQuN;Au7mFXAGA3c+XpOu z+5BCXyf1#x?VlscAjvSh2l2zs`>CR`!|`YLhJb{qWuUWVL&ywEN5ldYTSXbv-XMGk2dyryB5VL*6w~gKtA8LhR{~=tiF5 zL7B#eGYKl(6QOkr{~W$LySmUM5K|^&d1V(Qa-nK{vv9_H!OLBupbyDCI1G>5D0pHKtNfO-(=x0Sx5gT^ z3jPQZ-rcx({}H-DIwwV@uy%C*9Q&VFXUQJKW$k62<+=bvPwcMmA)=Pp)lI zF|H5qTngiV>({<&#yad+l(%YKTf-X7#J>M-C#bzHWc=x2}OBSLklET zhrheub?$s$%fUM5z2>Nz83iT-xx<7v>}{$^($zp&xpw}Z+)XJ45Ii(hzT~!-N}@&( z{<3$ss6>&5Hn&48_vO6DFhi5=b@N%O;%!Ky<1jeG%6$BuL1Dy?!fApowW0g=>R!+e z?WsF*<7HQcevbO0Ab&L4G2wF^DOaZ#DD_V-jj?>iu7lsc;w3*XHPyMBo`~RsT117F^jO zQ?0-otXlbDL^`WX%8rjl!Q8E-xSRd|NO${NlF}=PcW6gZbFBP)0<5pISRP`z_3MuF zqw()H^({-SV4k`p{r0#G{4+P*H_Km6&o!FmTPNFeT>;uR-hQ(jMVl8i-$SBk7-4ku z2o~4RZ;9K9bv&A9HO!Ht2gn4li3V=!6HT2tRZFsZW|V|;b_0$Cb`&k%PXnPJ3j@N8 z!?l3j*tMIH0J8%`4b{mh%1lYG_-pj-zScic(`owZcO|`6aF|4aH`$sfPp|N5{G!kf zcWp4Hrz+HL=%Q%ZhWh2a zom+I>Fs#pZ&^2}L)^Cs|py-u2k*aFTSq0zzPBrH7VE$6n>X^)kslO#*mx3v4_BXKe zOX_W144!KF(UgTJR{d$I2^JuGIC9FH3m|YcqCf;SZ>QXSTlF7j%IS=GEU(TBxUZDn zt(nCju?Xg{@vI^+I%RaNi;r`&Lk`>wW7EbpE&kJL==Ft6LY691LUzKICD=VG^Vjgx zMwjs1`B^vA8RrCll7H$n-fz0b1N!9MOAYk?hd%E|19i>Mw@I_Y%<6pf5Evsgv54Hi zdxB!+%GbZ)%)v=OKu{dJp{ZS>p;NC(a?i+RCmc3)Ar>%i1XT{bbQ13B28F9@&&<|y zM^$ryuoFs%Jp2l3f7=y)m;nfno}W}xx#NQn?q{aG$YGjpoPqc>FVfAuGR-Z)WZ`47 zE0u7nU>*SwG4vL968!T!eek>*xD9@3O>O{yK(g>M7vQyN=5f|dHglO|nZ9#JP?Bk^ zL1f?Rx4ORhr9$s(y~TD*%UoQA8?Z=O44aPlB#72_{g&C!QXWg(1P)Z`@_4-Y15@mV9tJhhl;5|IK+wy(O#oUMwG5+#hmPHJnHHG*RJP1#(Ni_n&?kH@?2HGbP z$(QZgjQ95&WMAF5Z1`*&bXz>Xt>U0zd`rrs_@E1|)7(F-R<~W5QIF}Tde=9Z#WE5+ zHQ>PaDXRV+PUj#*DMNvtm+cI&-5+XS-QO69Nn=WIhS^O943 zJ!rbFpjzQO@jfLfoN0_t{gVy>?awpKulv!X&rR=(^LBpDtKi;)G|YML7J`D@^AJ+L z=8u}&HPDDXGKnS??`z6@!V@$_%o6b1J!b<9QGkm1C1C%D#)=?FouH616GlyeW>cjOA z?n@m8x4qbZq1V{&3++-gj%oB{iCCqVG+*!FtoL@F5MDN0zt?-IRoWE&n&|##4(4BMI$dU^LnRbNNyI&-JJWSS0%kg{kkxt7Z&I& zCACr-)FhUtX83x4m#*pG2n+kIJzoqI~ucWq2xM$|oLx3`|j85oWcbVsbW;$^T< z(s>e$Zh>&w2={|?~K+QaLjTZ!hC+kG8t+IDo z(}8)>OSWe1alAf+|9ufnf#A7E68aXYaoHVry$S~SDDpX0Uub8~C32l%;7gQeZH3UW z$bqA#)pw$-RRmEA9MCBi215EqYS4wN#u#6vd3vRJ!A_(@G_`!x`d)BnQaSyDhuskc zO8jK&)(49u#N=F=CMOsUTTs_Ce*Wq=bX1QuQ8j#MBKH{&%mtB$Q3ZoT*K84{NswCK zPzbD1&~XMg;YGwQp$*)u70RwvbNeq*(R?y3Ey@+L_EkrnjjV?gfWYGmixAWWV{gN+ zg@Bm+bNUZ|Lz>O2AcD->)QR}GH*~ubOh`hJmyjo5yLyBV%YS}Os19$gh5izS}w zQm!n02;9juj8dX`z3h5}PE@x~FN6O32t@z&_#5zmvIzMfqLA^Sh4AX@Z20CgX}in) z#c_p)B0n~Y!?1#4m4KL+aS3RU<`WQ3(K-|6>)*8U34$*@#GvQK`St7D$O4u0KB|sF zMkW=>2%dpvfFo3+a+m4Cd-j5=>3_qfVCZ1^pmmx3Rz(IyX4N&1I-7>uDI`=EIP|gf zvba5;25^b2GFkoo(TQ4gOiiK|O;jqY`0Vq9ngmgj3qRGx%xbMz#i(SnpX9k7N4L0L z+^uy5JYGo!oPKxke`3mf92~s9p)7_@0(DUr$L~%spxbH6?!JR+GTOqtoz)I@UAhb+;2O zm&sPrcQ8cLXGaEUzM__!NYdZLsd+MKKyZKgA|>^~fsec*i?~F(zmSwYf-C>X>4(Q6 z|F&Zqho`K~cLV}m*5>ManZ7}Zz2VY|I*hg9K8;}Z5oE|pvb4`fQDBI}x|LacNy>sy#`29=u-#|gbJ zXWpD1veuptn}>Dg86gk!3V8TKWbIpS)?1#Nut0UJ?TGYvQBWb3<0O2jv_+7y4z7`W z5pnC@&)-u^x7V7MQ8+x?Jq#lG6=J-h-zb#{FMy##5}u0-kk9iMS{^%t&3c#J8=Ynp z7ja=yKR@SEgsk1Z-Is9d`OV{;w;#0;40NWY(p#mp0~0MHUC=EAYEvbo3n9sgy2GO< zN#bNR;vyI@oQh$yK9-3t$-?a-e9{oS-pi!I|W=d$5$3EiF#e=eKD&IdqY@PW(N%_%`Byl2xDyEi7LP z)h#QJZoT^}Z$4HFS{u6o9m>lH0`YFK(g&wiZ6@*?81W03q^gUKSo8xrAYCXr22TwFYY1ra*_k8Xg`wp3R-yHnY()jJ^lJRxGNGDE%CNX)d*}1}-1?w`y5K@4M zP3%wXJTb?66~pn3lEJ~naVpZq#x1qZ1?Jy}M7tf9&E^EMuVoJ*G@Ez7HQvwTGn^X4 zlb~y}r~9Z>ePRFJY-cfPyXllLK7q=&Qia!~h8_IZ(3i*lWVYSW*z*$OHY?8>tb;vG zWCqjgRfxbMIEb6aSc-EhGHd1bZ(_Wlv6#O#M(>q;eK*eRxG!a`m@W;KXg2_#g=o-xC{ zC`Vx|`v~r~fms*NFK2g<0l!*4iQzSth9(t!`GJWA@<~jN_k$@X_rQ;T+b@Bh%Jy}4 zVd7yuxmNYi9t4m`=TZMDiHcJ ziO}h2N3e9>HO}4Jbsf>oB8**`yLO3l61=Rf(#ABb9(*Il*euCRRb8 zV!myqtLT0O_>xp=HDJ3QTD%qq-o)=Dyt1UCuqT13iWnrj?}B>ZO`<7WJIl%=W3ahaGd= zGuAxhf8ms=9q0M<9`}9(4Wm1Bd?&(83I4r2fWkz>4}fWX=E0AKCik&->f<$yaUT69Ar<9!N6A!iZc`zt(*T?l)a&?q61u$2Z<+qV!g=x+IR2-L5BTzW-Ebih|MmUNBgH z`Q%wvZtSCKT@hJ`a`JC7+_rPpzWS>NnG8bk#}#^q7%$=H-!ZgspH%vW++(av;P<$ef@*-O-mAQxJXqwQupDw ze>cqKlpJZf?K`|8kEtKl*75S)6C6Nhm8q@*Q4PP!NkYqX*et0ZAP`0~>5mP_yys9S zb-Q_6Qh-he>q2l~jZygRDRt@k!eD644e|B)Lh56}EO~3yAZ?9>K0Z&ow zKjv}*gH{zKL+^;70DGE9K;^#-*ExUWDb`EzDir6RmO+TE%XpwD>+@x%klT1o= zsr)!MW$6q*n$(}+7bxIO2aCpBvJ(zGC@X5n&Ws_5Dl2*?s>^OiL6y2<9-vD4VkN$W z$w(63eXXg&MsLoM{=Y)&N@!@}9YcH2V9br)CX#?ZS(}ge!9Mnblao6KK9&~|q10n2 zrn5y29W$~Th5!?T@3mnc*SCvEj(dmM?A$@IQ7Cx+gKB|#vXN07 z{VNB9VoG$-2gV%Xd1$F-==q&p}%31vNT<5cdy|v5718At<=OxPG zJQS6e@)&Iek5vMwUur=1f92|dTd3g)Rc}p+F=RyFq$${40Zd{@+Gkuo<`$R<2xyUt zjt>R&Q?GGz*3>?%mscD z8}uv^u^z3+O_Bgh#;Kr?F+i(W)_|qbfzc%LiJK9N8t2_3s49})RipC5jO#GBfUlCu$f}XT;5IG1|?0_zpV~Kqv;Z|6492C6zRP3 zEs{EGsefcx(ahicicr}St;_^yENXp-e?Z7j$f%+jB zpGB7*_(z(q0%CL&DB$*_=I(r4R6l*1S!YQbK7**7d3nlR&zy^vuwV)YCe(Hhm-n;gx|0=~U!7OhchKGF@1HG! za_t|lURWzBLn;OJdYVFzmuIo?p9VC?70&f6Cl=aks$$0%vQmP@MhFiX-B@HWf7E_Q zU27>G+xehR0z|u|D$I8!Pb%%ebhg=iRB~UeXuFvw(DQ{Q&@;8NB!rTX;a@d$6MTodW&wcwadBmG)Z$&eBl}ZmqdH77N{vm8UjF4C@Shli&Irc~mRTt%ZaRtB{7Um76fm^0aZIm+F2iO3Le z`>vm>1#nnGRShWzb##zZM>kPlOoqNzLE1UWY$`#g`{G_(9v0Y!yh@3Xx;vrOA|PU zQ>XBNNrD3Lq&5w`LnNfY~dJz+@^3L%0>9OMO= z#3PxTJ@z(|IP=$Ki~75UufH}N)o|7@zDD`ygg#$b9L93jJQtIsbwcpH`I|uF=RK;1 z2)wdl{B|Jxl-}^)jjna0lMI1HlE-!!xSHh@7!!8MudD?qs=tVx{>d7*R$1c}(b+W* zSBgrdvrk2Y^zYMYe<_sD#Pg50Qt~y?kH*M^1f>Q%vpIP7kxqjbNywqNg_}|G<#2VA z!|7zdSzsWeHB}U^ay>L?jk>l-bunUyG0qen&7f|0<_c>Q%IJ{=Sm z0!{=bw12mUb7P&loSu>RC?*0{E5(d*6-lnC)j#&f()g@saN_-ONfd>%%G^#8D)H+Q zvIGk+Cynbe&pb9JPY?K)z0Mk!Ds_H!pK+OPln^pf;+mR?{*41hW3#}^(mw@fw#=i; ziN`z=s8LMOomoYblf=4@f>i%q_E_7m@jHsnqhXzb>hmd&nlzzt=JYbVa3-H>BQm3O zeB3AYg@3M>DOGs=9jw;9oK3a2mscb9$4~#Uahdc}qvKU!-KWcO%*u(k<rK2Lu9j`FXjdf6cZ&9~mW!DbJ)hxL2&bW z&@4^MW2IHEC9taX$vCQUB?u(e?W#%TX^0rMY4bh= z{7T_tJ}=P55uf_wYl^g7$fY#0WExJ9=quCg%6-KI0eg32z3Azd9~1P zXnVTI#wBDL?j|f;tj0?9Fqq+-zK|SN5t57NyGaa8v^IxG`fo!6geypsF=gdc|ZHIUf+ zKzQ3lJB4?Bglj>2bPYX+q3DzLqV`w9ye!!Yp+`7$4YFnb?)oYds3l$9>99r^)%Y*_mFb@tRHP>A+?4Y2h1g@wO2Z z%A*N0I3LuFeP($*>=tCkIzJjQ++MD2R=QrGQ${=S6dqma{IM;a(I0BWEhf^8WIO-( zw#CTQxwy%IthFE|9XksXE+HN7Vl**JEQc#)D2}kGsADv#XNfTIgGXU_EJ)EONME0| z);2vb+|?V+`%zZAe1X;N#i=1)b7}=gDZk#FQ`RZ|>b=m?!zh37b zaksTF9}^s|P+}7foeT;6~7nACYz20B@HGFEF?^`^YoHO6&NPes6R?v6SS~rhW zK+@)#pGNLVP}ZMjqnSOQVr^L7f(1Dy?9ni2pdYe_*4BeWx%>>~_q~K051}d3^JUre zq0fBUl3HJm#6DMq{3oL)O&~zPlhQ;ia+8SsrsY`tLCmWzd#bTfVNYqkpDW#lBK5ah zP^X1fvY5tjDY<0|C;)@9Fi4XsF=QdVu~4d_2wGtN%uU+Y--cE=2lee}%AEY`I=z8;yh#< zUrP@tYk(%f7Yjh2Nd9q|e>{V9*q}{oG#U+C8F)XMPpj{(gSE3*J%@iI7m2K8pabJpAdl?}yM`eSTdfslQdE z1`5RxQWoWk>MvR|@3RC}UKx*h;?Ij!7V|SC(kTi_it;#(t4L)2@jU%Oo%iHWnUsz^ z=;y?Q@e``;rZ2&A71`;EFTmcNyR-5hbiN{UEYOvsdV?<=l6yJexVcymsn6$#o%d48 zzAF{5s$Dyc8Y#IOD&iS>-Gp)+J!K`{zWJpk{5i;}?B;eLhh!o_BS>miJf9lMbtroV zDr1w;;Vw`rOT%j0IA_q9glbG$bU8+N`YW$oG^8>;|BgrrcudW?TJwqb?jtd`Jb+>c z(f>X+HFYL)#pj1YPd&S7S$@Zb`$ug1yA!>=$H3UoMk3f$6Zm0iWxjCtw#1NRDxMbW z(@GRyjJrqDvf;o_Ww*-d_+pqpytzDUM?Qr#PHufFsbM#DQEze5!whL2Hag(erf?H7 zwG(RwRsiCleJmotkFuC0IJhfG%sPQs*>^Wz5+jlGN01Yb=s#;zN;a>^GaAHNH_ph& zgNqy5aQdyHroWgzKRb_4`Z%IPz3ctsdwO9$v#@G~5#0l0tt-Al4di#f zv+h>CAsL-#biTaavl}<1nq_G(Pye2=p|ZsrhadH^HJ5e^5&J%sfSdf+#)?Thj!`i> zNMB&4)of##!L9u@)I!&F`##=x;$rIbHkF%00EXjZ=XSz&6hk6L_eBo(jr)2)mp(9n zX6AaP7eA?T*oL4}pf? zb{SODoeD8&=#?*|@IF)r5uvd^4DiG|<)tgLkVzAzkR0E>(8dw%@yev<&k|D6P*Uk} zN%GAMl#ofZ$DX<>&sUzhPFHeCU{xxno?q`vI9dKp`htwzp+XIoKq6v}1NZMlsQ6V=?4uG7Df$@p$=uaBBEwhvrJr)QS z!Opo;cC47Sl9uGOa6FZqJaXBGK)i!4C-M%oZvtY*Atjeol&B}+1e(3UV(4T4SY>U* zt=57yJdNnxet~mUyn~;1az&G)R79HM zYv(^|x)|kB`>ljM?#Rn`89KD@(i%%LU-~RM&+KA<>+}ghg>`80j2T>I)oYJ^oQuBu zHu3sK>+o|bLR1;c{s)D!08B-&c1ThKMy#zeXFCqQWZly;%KWpV02EzqDA#fO|c+rOI?cfFxhf}KxcWSO-UvEamWbxC6M zPtjQIDGTqghqDN)SBCR2dRjc?9dSc6t!#~Y7)-GfKJt%A}uf-e=&?x2MqGSB7xO=UWz}ZQ{Uc6 zCGV!zqxV?mK>z=IPsi5;M8U4kKibj1i$8=VW9eWIT{*r?XYxMy1$sl@ zg80NecpB7ki)o|stzW%dMRSWJ%4#Z`to=zeP{A<@U)74~<><8Fw_&QL@fl8vl8f39 z&p&#f+`7Yc!bR3?yPb^2Bz+gx+5Zg89O8>`-TPWJo=u&h^tqj1udwP8^yHvg9`_!f z+V!#@iz(mtQ4|X4?gTTq2Httw?QWZe?$csV>Ni^J7Jcem&gCoT8wkV*O)w`h7H5W* zGx&ALEX5qFWvkx+oH4!I^CcwH^RYz|fVNsYdW0?pE8LVJBC>;RZ%o?XD0?O2UC`+Y5ZC8-|4B2`;Kzw%vSqA&BX3p%BL9+de7(llyrLLf^$AMkFms-Km+)eQzR{Iz@xDOY-LvU zt)4#8nDRY-6!v|IYSif;Aix;(kQ48FgI3d^;xDMm@-f5x;`m*&+vTwHkVI9eJ(N!z ziJORMb7u03fLM$(gCXOusM;1i`k7cAgN5~zz;N`v0*eI%5|b%q@?Z)sb6(P3EA9Ny z3AsYB=HayVZii$kDwdF%;@+;1Vzwo3c{%2yEfWRtkrOEI4u-So_4)bbw6U#Fe-Bde?3L*J)4AiOUuijcob$zuXkZjN4z=aN z`)m${k-CaeF1#YNj|%@8$x>{euizyfzl$~tMQPa zh5Tso+SWw5BHijqGvu3<((PLDxJEMa?HmvIX7{O zxO68X)K^4qh&B{?>WmrU^0_%w?cM@(O|~dgJ!$GYKWE@PbCz^6KWQ@gTzN|6VJY6{ z|KCf3NPS!<{0@nvd3tRW?s#f10jy-19UM-5|Amw3F^6MSNKgk4l{d(K+j})^N*F|5 z_RYx${>sdxWpI(TnRW_siUUT52XRjFX4dw%*%v2bVCG$2{xB?i0YX!yfA=Tm5I-+! zavC`d__bPE8SW`IbNphiPG(L{<1^cp%EtP3X1d=z+CiTLT-|Sy;I^V zXI#&@24wq_Tes2)=;IQBu#;$!ArJ_yOb|FxQ)+x-0?SGvy_nDh7>i8Qfl?5MenDMr zQJLZ~bw748XSRM($@c_WbX~}C9PA^u=- zrdiC_rQXp}W{eO$_Q&3x^JacWi@uUR@g>DCtoGW)pNi_T7N>%+ z{t-GXy8jSM{V!%~x*z-)vv)B}e6c&ep*KFBEuHN!>B&|e7q3~eQ{4bOE0F59-8Oqm zMcH(fWe5)XvG9p{a>s$4dGqIqW<>Z>g>#o}MF0itQQM_|hlhx;tJaO)U0MDf@redZ z0G-WC8}79ygSYdS$3fDfZNvYqGlqtKS@+Wbi#zt5fw0Gg-;jOxVYW*meZay?_8EXN zN4II?wmyYbzhBHq0c#;2dDj3xdJ)D#JaPq{q}Y{E!NdxI$2yF}pbt+-7YWWvIaqaB z2^cANTti5%fBG!6;d3?s3wsY6>|^L8Vu3pyfG%K28y(_Ge#u2Hk25f`{ZP(M1R~KR0WsMar&7&b zkmgAb>P0eRjMJ(PPEzya{&GBE1-io8%}?r1HwE@glWUwVJ@xNJxg-4~=pUg7+p&gv z1L}s7z4l%?Ri(dS<-kaQpS>OZGQxzFwWj`C$Fwyf8NDgpOlBo&r2Zq{EYi_ck<_>J z-cWmMGp$)QZtdMloxMTikL$4JAR@1_d{Rc{nfOft;@19OdWW#_1BjYq%v!Ga^Q5m+ z!HA?!fpm9lVg z;1{&3=M>dFThllm6%(SWmlUDK$NG)09>Oo%@Wi)GVdBu*J2xo`5|>?6m-|WO-2JBW zS;D>T#!JhQqO0)?iNNPX=io2VZXjBI+;7&~5g`(|bShG&-!)vxrsor5JP54^G^^T7 z$E{~bv|M@SKU$UzzC5r*;B&{v3O6kN^|@+?^uDZpBx4c(Q1u1Z=hY6w+Z%}hN57@B zx(xD%6EeUAi>=fC5B+dR3%6m2AJTbWU>$kyNh^c<_$Yc%39wO;w>20>$e1fTTK%f? zw)G1~sndU%t^EAwdVxpQe9SLSQiT67xw3@Q1n0_7>c`&z)twfcz~ z_tyZ^FYBY=()}?N{1s$|-;G)rqp?vdaW!26%xHYBju3WDiznB;r9n?AZ$aU+A($$X z^Z*eg;!v!E zuJqU9{l0wF!=T-Vde9d0^(90!I|~TeS&CDOk76vhORKa+PWIHW+|Lq0NOs8-{jLoO zhPz&==;uk6LJ9HLvam5ZEZk`QNndfm3kv8SgG z&!_jXup04ZBc~JCSj~;qA6fhiiUNy%lA`}kzwo`TrbxVW)N>Gjm@uXuh;Y|O4S{95c z&y39+qorE%KEX6DEDXb(UwNC_JY+_OJb-W$XpsjAqoAG93W;z<+$Q6|QXFPlu!<*X z7)%H<-~9%nDjEHyB4uGNd#*SYQm5T^uL!j-{bjx8;*q;Wr(uT6+PtFnqFjPL|02cO zwy*2^$>!zLy2y;_78)AHf2<=hdBlgCV&S)4_bv-1{^npjEA^^cuvB`_DQLfU%hF+@ zt(1ddWuZzsIR~gP<2*UQr`X6MXB8t_6p1;h$``$mUgt~^ogXnvNL^Vw$dZ1kuWen; zWZH-=Zl&fxCKlIlPH~E)@9oQy9Ps@&yHc%N2RQogO(JdSIY=v?_@ZWGJL`jkausbP z?xffeqZ6qY2JWOu@}&yU1l}x?3>>QWOS{Y0LJNNt^#$EzUvTE;`Z+(ctldYe$!uW7 zY<0?eCka2U{hu4`pJ~^8sFq3|y210dEyOQh17`W{4U%sl>p0x{kV>=rjSe$Op??nl z+;^ptgI|zTQ_6RE^qE8T=(BQqGyW2$7K1V^MZp$=N1>P-d@5<53_KlPEoJGUN>XU6 zD=|}$l+R@}M*Ur0nS%QIL9~O@&c}P2S4T7<2GrSx*xyA?sWFRQh@qA2u9Y<#G)0&C z$6J!HX1FJeCm2uof?b z-oq>lE7=3++K@o0KFfyztPo)7w(vX#b31+4)n?b6vA+^xvb>^9t?l7m17&43%PF)Fz>&7k`0Z<5*MYqG(>0B(I1*bJZksnR)kL zHc#TJw^@gs;!)bd&9&axXN@yQoRN;Ssb?seExhYgf|30arU7-m`V+v%)2?-4Z|NBV z#kJ6eQ_&i$q`e2S#8nMCk6Z6`3SAE~@6AdNH@$CTQjdjW^SHQr)$<{BbbquEdRjS*-hL0ok3Z>kS5~(3=g0?ND6my{8R1KuEJb62Ng#%Lr?d55U-~ zgYkKYsIs6}GfC?N;f!{o;`%F>Dg;zWkXetaQu?Ty#(Na?*wS7c@UlCl$@BU*@@@2X zY8<+^Jlv&o3N1H|3BJ*>TruRhW(GGF z@OE0m<*@0IrRT0sJN~B(hIR}&HvW~{vZAQzY+f7zs3J=@WB1|T>p(e bsz2`-ke8jd-bzRP56~easVGq;W*GE;+Aa+Z literal 0 HcmV?d00001 diff --git a/web/public/logo_simple.png b/web/public/logo_simple.png deleted file mode 100644 index 207e9d297102df82805bf41e157934a33ac4bf34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105870 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

NAFM|qBWq5P9YQ_X2=34EjT149+ERt1$-+j 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 Eg3hk)xc~qF -- 2.49.1 From 3c64a859b417d389355e9f119f9ce69ea66ca171 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Sun, 19 Apr 2026 13:33:52 -0600 Subject: [PATCH 04/34] Replaced emoticons --- web/app/ai-chat/page.js | 22 +++++++++++++++------- web/components/FloatingChat.js | 26 +++++++++++++++++--------- web/components/Navigation.js | 7 +++++-- web/public/bootstrap/cart-fill.svg | 3 +++ web/public/bootstrap/cart.svg | 3 +++ web/public/bootstrap/chat-fill.svg | 3 +++ web/public/bootstrap/chat.svg | 3 +++ web/public/bootstrap/person-circle.svg | 4 ++++ web/public/bootstrap/person-fill.svg | 3 +++ web/public/bootstrap/robot.svg | 4 ++++ 10 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 web/public/bootstrap/cart-fill.svg create mode 100644 web/public/bootstrap/cart.svg create mode 100644 web/public/bootstrap/chat-fill.svg create mode 100644 web/public/bootstrap/chat.svg create mode 100644 web/public/bootstrap/person-circle.svg create mode 100644 web/public/bootstrap/person-fill.svg create mode 100644 web/public/bootstrap/robot.svg diff --git a/web/app/ai-chat/page.js b/web/app/ai-chat/page.js index 883e6094..8a40af41 100644 --- a/web/app/ai-chat/page.js +++ b/web/app/ai-chat/page.js @@ -529,7 +529,10 @@ function AiChatPage() { {conv.status}

- {conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"} + + + {conv.mode === "HUMAN" ? "Live" : "AI"} + {conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}
@@ -552,7 +555,10 @@ function AiChatPage() { CLOSED
- {conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"} + + + {conv.mode === "HUMAN" ? "Live" : "AI"} + {conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}
@@ -567,7 +573,7 @@ function AiChatPage() {
{!conversation ? (
-
🐾
+
assistant

No active conversation

Start a new conversation with the AI assistant.

{error &&
{error}
} @@ -582,7 +588,7 @@ function AiChatPage() {
-
{isEscalated ? "👤" : "🐾"}
+
{isEscalated ? "👤" : assistant}
{isEscalated ? (hasStaff ? "Support Agent" : "Leon's Pet Store Support") : "Leon's Pet Assistant"} @@ -617,7 +623,7 @@ function AiChatPage() {
{messages.length === 0 && (
-
{isEscalated ? "💬" : "🐾"}
+
{isEscalated ? "💬" : assistant}

{isEscalated ? "Your conversation has started. A support agent will join soon." : `Hello${user.fullName ? `, ${user.fullName.split(" ")[0]}` : ""}! I'm your pet care assistant. Ask me about pet recommendations, care tips, supplies, or anything pet-related!`}

@@ -634,7 +640,7 @@ function AiChatPage() { ...(isOwn ? s.messageRowUser : s.messageRowAgent), }} > - {!isOwn &&
{isEscalated ? "👤" : "🐾"}
} + {!isOwn &&
{isEscalated ? "👤" : assistant}
}
-
🐾
+
assistant
@@ -930,6 +936,7 @@ const s = { height: 44, borderRadius: "50%", background: "linear-gradient(135deg, #444, #666)", + border: "3px solid #666", display: "flex", alignItems: "center", justifyContent: "center", @@ -1113,6 +1120,7 @@ const s = { height: 30, borderRadius: "50%", background: "linear-gradient(135deg, #444, #666)", + border: "3px solid #666", display: "flex", alignItems: "center", justifyContent: "center", diff --git a/web/components/FloatingChat.js b/web/components/FloatingChat.js index 4e54546f..741db28a 100644 --- a/web/components/FloatingChat.js +++ b/web/components/FloatingChat.js @@ -20,6 +20,7 @@ export default function FloatingChat() { } = useChatWidget(); const [input, setInput] = useState(""); + const [fabHovered, setFabHovered] = useState(false); const messagesEndRef = useRef(null); const prevAiLengthRef = useRef(0); const prevLiveLengthRef = useRef(0); @@ -65,8 +66,15 @@ const prevLiveLengthRef = useRef(0); return ( <> {/* Floating toggle button */} - @@ -77,7 +85,7 @@ const prevLiveLengthRef = useRef(0); {/* Header */}
-
🐾
+
assistant
Leon's Assistant
@@ -99,7 +107,7 @@ const prevLiveLengthRef = useRef(0); {/* Guest */} {!user && (
- 🐾 + assistant

Log in to chat with our pet assistant!

@@ -129,7 +137,7 @@ const prevLiveLengthRef = useRef(0); {!convsLoading && conversations.length === 0 && (
- 💬 + chat

No conversations yet.
Start a live chat above.

)} @@ -223,7 +231,7 @@ const prevLiveLengthRef = useRef(0); return (
{!isUser && ( -
{msg.senderRole === "BOT" ? "🐾" : "👤"}
+
assistant
)}
{!isUser && ( @@ -282,7 +290,7 @@ const prevLiveLengthRef = useRef(0);
{aiMessages.length === 0 && (
- 🐾 + assistant

Hi{user?.fullName ? `, ${user.fullName.split(" ")[0]}` : ""}!
Ask me anything about pets. @@ -292,7 +300,7 @@ const prevLiveLengthRef = useRef(0); {aiMessages.map((msg) => (

- {msg.role === "assistant" &&
🐾
} + {msg.role === "assistant" &&
assistant
}
{msg.content.split("\n").map((line, i, arr) => ( {line}{i < arr.length - 1 &&
}
@@ -308,7 +316,7 @@ const prevLiveLengthRef = useRef(0); {aiSending && (
-
🐾
+
assistant
diff --git a/web/components/Navigation.js b/web/components/Navigation.js index a9a2af98..2112a6a7 100644 --- a/web/components/Navigation.js +++ b/web/components/Navigation.js @@ -14,8 +14,11 @@ const cartBadgeCls = "absolute -top-1 -right-1.5 bg-[#e53935] text-white rounded function CartIcon({ itemCount, onClick }) { return ( - - 🛒 + + + + + {itemCount > 0 && ( {itemCount > 99 ? "99+" : itemCount} )} diff --git a/web/public/bootstrap/cart-fill.svg b/web/public/bootstrap/cart-fill.svg new file mode 100644 index 00000000..974fc295 --- /dev/null +++ b/web/public/bootstrap/cart-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/public/bootstrap/cart.svg b/web/public/bootstrap/cart.svg new file mode 100644 index 00000000..0e0f96ce --- /dev/null +++ b/web/public/bootstrap/cart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/public/bootstrap/chat-fill.svg b/web/public/bootstrap/chat-fill.svg new file mode 100644 index 00000000..c8969390 --- /dev/null +++ b/web/public/bootstrap/chat-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/public/bootstrap/chat.svg b/web/public/bootstrap/chat.svg new file mode 100644 index 00000000..487d142a --- /dev/null +++ b/web/public/bootstrap/chat.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/public/bootstrap/person-circle.svg b/web/public/bootstrap/person-circle.svg new file mode 100644 index 00000000..a75f25fc --- /dev/null +++ b/web/public/bootstrap/person-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web/public/bootstrap/person-fill.svg b/web/public/bootstrap/person-fill.svg new file mode 100644 index 00000000..46d1a75f --- /dev/null +++ b/web/public/bootstrap/person-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/public/bootstrap/robot.svg b/web/public/bootstrap/robot.svg new file mode 100644 index 00000000..a2242026 --- /dev/null +++ b/web/public/bootstrap/robot.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file -- 2.49.1 From a85e295c30573e930ccd13e7e3dbcc2d0091d1a1 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 19 Apr 2026 17:12:29 -0600 Subject: [PATCH 05/34] force logout on 401 --- .../activities/HomeActivity.java | 28 ++++++++++++++++++- .../api/auth/AuthInterceptor.java | 17 +++++++---- .../petstoremobile/api/auth/TokenManager.java | 12 ++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java index 01b4ca24..0b2d8a1d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java @@ -1,7 +1,10 @@ package com.example.petstoremobile.activities; import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; @@ -20,6 +23,7 @@ import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.NavigationUI; import com.example.petstoremobile.R; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.ActivityHomeBinding; import com.example.petstoremobile.services.ChatNotificationService; @@ -30,6 +34,16 @@ public class HomeActivity extends AppCompatActivity { private ActivityHomeBinding binding; private NavController navController; + private final BroadcastReceiver forceLogoutReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Intent loginIntent = new Intent(HomeActivity.this, MainActivity.class); + loginIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(loginIntent); + finish(); + } + }; + // Launcher to ask for notification permission private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { @@ -67,10 +81,22 @@ public class HomeActivity extends AppCompatActivity { handleIntent(getIntent()); } - // Start the notification service and request for notification permission + IntentFilter filter = new IntentFilter(TokenManager.ACTION_FORCE_LOGOUT); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(forceLogoutReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + registerReceiver(forceLogoutReceiver, filter); + } + startNotificationService(); requestNotificationPermission(); } + + @Override + protected void onDestroy() { + super.onDestroy(); + unregisterReceiver(forceLogoutReceiver); + } /** * Handles new intents received while the activity is already running (like notifications). diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java index 02bbe3c0..0feac644 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java @@ -23,19 +23,26 @@ public class AuthInterceptor implements Interceptor { String token = tokenManager.getToken(); String url = chain.request().url().toString(); - if (url.contains("auth/login") || url.contains("auth/register")) { + boolean isAuthEndpoint = url.contains("auth/login") || url.contains("auth/register"); + + if (isAuthEndpoint) { return chain.proceed(chain.request()); } - //If we have a token then add it to the request + Response response; if (token != null) { Request request = chain.request().newBuilder() .addHeader("Authorization", "Bearer " + token) .build(); - return chain.proceed(request); + response = chain.proceed(request); + } else { + response = chain.proceed(chain.request()); } - //If no token then just pass the request - return chain.proceed(chain.request()); + if (response.code() == 401) { + tokenManager.forceLogout(); + } + + return response; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java index dc6096de..8d53f8e7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java @@ -1,6 +1,7 @@ package com.example.petstoremobile.api.auth; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import javax.inject.Inject; @@ -10,6 +11,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext; @Singleton public class TokenManager { + public static final String ACTION_FORCE_LOGOUT = "com.example.petstoremobile.ACTION_FORCE_LOGOUT"; + private static final String TOKEN_KEY = "token"; private static final String USERNAME_KEY = "username"; private static final String ROLE_KEY = "role"; @@ -17,13 +20,22 @@ public class TokenManager { private static final String USER_ID_KEY = "user_id"; private static final String PRIMARY_STORE_ID_KEY = "primary_store_id"; + private final Context context; private SharedPreferences prefs; @Inject public TokenManager(@ApplicationContext Context context) { + this.context = context; prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); } + public void forceLogout() { + clearLoginData(); + Intent intent = new Intent(ACTION_FORCE_LOGOUT); + intent.setPackage(context.getPackageName()); + context.sendBroadcast(intent); + } + //save login data after login public void saveLoginData(String token, String username, String role) { prefs.edit() -- 2.49.1 From f8c985efb5d2469af4097501253b3be486705800 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 19 Apr 2026 17:33:48 -0600 Subject: [PATCH 06/34] fix read state tracking --- .../api/ChatRealtimeClient.java | 25 +++++++++++++++++-- .../controllers/ChatController.java | 7 +++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java b/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java index 93f90a73..a505c514 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java @@ -14,8 +14,10 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicInteger; @@ -43,6 +45,7 @@ public class ChatRealtimeClient implements WebSocket.Listener { private volatile String currentStatus = "Chat disconnected"; private final Map globalConversations = new HashMap<>(); + private final Set readConversationIds = new HashSet<>(); private final List> notificationListeners = new ArrayList<>(); private boolean lastNotificationState = false; @@ -79,10 +82,24 @@ public class ChatRealtimeClient implements WebSocket.Listener { if (conv != null) { conv.setLastSenderId(senderId); } + readConversationIds.add(conversationId); } updateNotificationState(); } + public void markConversationRead(Long conversationId) { + synchronized (lock) { + readConversationIds.add(conversationId); + } + updateNotificationState(); + } + + public boolean isConversationRead(Long conversationId) { + synchronized (lock) { + return readConversationIds.contains(conversationId); + } + } + public boolean hasActionableChats() { synchronized (lock) { UserSession session = UserSession.getInstance(); @@ -98,7 +115,8 @@ public class ChatRealtimeClient implements WebSocket.Listener { // Needs reply (assigned to me and last sender was someone else - customer) if (currentUserId != null && currentUserId.equals(conv.getStaffId())) { - if (conv.getLastSenderId() != null && !conv.getLastSenderId().equals(currentUserId)) { + if (conv.getLastSenderId() != null && !conv.getLastSenderId().equals(currentUserId) + && !readConversationIds.contains(conv.getId())) { return true; } } @@ -302,6 +320,7 @@ public class ChatRealtimeClient implements WebSocket.Listener { conversationsSubscriptionId = null; conversationMessagesSubscriptionId = null; globalConversations.clear(); + readConversationIds.clear(); updateNotificationState(); } @@ -352,13 +371,15 @@ public class ChatRealtimeClient implements WebSocket.Listener { .notifyNewMessage(message.getSenderDisplayName(), message.getContent()); } - // Also update globalConversation last sender if this is the active conversation synchronized (lock) { ConversationResponse conv = globalConversations.get(message.getConversationId()); if (conv != null) { conv.setLastMessage(message.getContent()); conv.setLastSenderId(message.getSenderId()); } + if (message.getSenderId() != null && !message.getSenderId().equals(currentUserId)) { + readConversationIds.remove(message.getConversationId()); + } } updateNotificationState(); } else { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java index 4417bd23..3ba9e995 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java @@ -282,6 +282,8 @@ public class ChatController { realtimeClient.subscribeToConversation(newValue.getId()); } updateChatState(newValue); + realtimeClient.markConversationRead(newValue.getId()); + refreshSections(); } private void updateChatState(ConversationResponse conv) { @@ -320,7 +322,8 @@ public class ChatController { Long currentUserId = session.getUserId(); boolean needsPickup = item.getHumanRequestedAt() != null && item.getStaffId() == null; boolean needsReply = currentUserId != null && currentUserId.equals(item.getStaffId()) - && item.getLastSenderId() != null && !item.getLastSenderId().equals(currentUserId); + && item.getLastSenderId() != null && !item.getLastSenderId().equals(currentUserId) + && !ChatRealtimeClient.getInstance().isConversationRead(item.getId()); if (needsPickup || needsReply) { title.setStyle("-fx-font-weight: bold; -fx-text-fill: #FF6B6B;"); @@ -460,6 +463,8 @@ public class ChatController { vbMessages.getChildren().add(createMessageBubble(message)); if (message.getId() != null) renderedMessageIds.add(message.getId()); scrollMessagesToBottom(); + realtimeClient.markConversationRead(message.getConversationId()); + refreshSections(); } } catch (Exception e) { ActivityLogger.getInstance().logException( -- 2.49.1 From 5fd7a333cba7d8aa6777330b459b03a1818867d9 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 19 Apr 2026 17:42:30 -0600 Subject: [PATCH 07/34] flatten flyway migrations --- .../backend/config/DataInitializer.java | 15 ++ backend/src/main/resources/application.yml | 1 + .../db/migration/V1__target_baseline.sql | 7 +- .../resources/db/migration/V2__seed_data.sql | 243 +++++++++++++++++- .../V3__nullable_appointment_petid.sql | 3 - .../V4__drop_purchase_order_status.sql | 1 - .../db/migration/V5__seed_data_2026.sql | 87 ------- .../db/migration/V6__unique_constraints.sql | 1 - .../db/migration/V7__fix_service_species.sql | 14 - .../db/migration/V8__seed_activity_logs.sql | 90 ------- .../db/migration/V9__seed_recent_sales.sql | 51 ---- 11 files changed, 261 insertions(+), 252 deletions(-) delete mode 100644 backend/src/main/resources/db/migration/V3__nullable_appointment_petid.sql delete mode 100644 backend/src/main/resources/db/migration/V4__drop_purchase_order_status.sql delete mode 100644 backend/src/main/resources/db/migration/V5__seed_data_2026.sql delete mode 100644 backend/src/main/resources/db/migration/V6__unique_constraints.sql delete mode 100644 backend/src/main/resources/db/migration/V7__fix_service_species.sql delete mode 100644 backend/src/main/resources/db/migration/V8__seed_activity_logs.sql delete mode 100644 backend/src/main/resources/db/migration/V9__seed_recent_sales.sql diff --git a/backend/src/main/java/com/petshop/backend/config/DataInitializer.java b/backend/src/main/java/com/petshop/backend/config/DataInitializer.java index 9809ddc6..89cbb18e 100644 --- a/backend/src/main/java/com/petshop/backend/config/DataInitializer.java +++ b/backend/src/main/java/com/petshop/backend/config/DataInitializer.java @@ -34,6 +34,7 @@ public class DataInitializer implements CommandLineRunner { admin.setPhone("000-000-1000"); admin.setRole(User.Role.ADMIN); admin.setActive(true); + admin.setAvatarUrl("/uploads/avatars/001.webp"); userRepository.save(admin); System.out.println("Admin user created successfully"); } else { @@ -67,6 +68,10 @@ public class DataInitializer implements CommandLineRunner { admin.setRole(User.Role.ADMIN); updated = true; } + if (admin.getAvatarUrl() == null || admin.getAvatarUrl().isEmpty()) { + admin.setAvatarUrl("/uploads/avatars/001.webp"); + updated = true; + } if (updated) { userRepository.save(admin); System.out.println("Admin user normalized"); @@ -86,6 +91,7 @@ public class DataInitializer implements CommandLineRunner { staff.setPhone("000-000-1001"); staff.setRole(User.Role.STAFF); staff.setActive(true); + staff.setAvatarUrl("/uploads/avatars/003.webp"); userRepository.save(staff); System.out.println("Staff user created successfully"); } else { @@ -119,6 +125,10 @@ public class DataInitializer implements CommandLineRunner { staff.setRole(User.Role.STAFF); updated = true; } + if (staff.getAvatarUrl() == null || staff.getAvatarUrl().isEmpty()) { + staff.setAvatarUrl("/uploads/avatars/003.webp"); + updated = true; + } if (updated) { userRepository.save(staff); System.out.println("Staff user normalized"); @@ -138,6 +148,7 @@ public class DataInitializer implements CommandLineRunner { customer.setPhone("000-000-1002"); customer.setRole(User.Role.CUSTOMER); customer.setActive(true); + customer.setAvatarUrl("/uploads/avatars/015.webp"); userRepository.save(customer); System.out.println("Customer user created successfully"); } else { @@ -171,6 +182,10 @@ public class DataInitializer implements CommandLineRunner { customer.setRole(User.Role.CUSTOMER); updated = true; } + if (customer.getAvatarUrl() == null || customer.getAvatarUrl().isEmpty()) { + customer.setAvatarUrl("/uploads/avatars/015.webp"); + updated = true; + } if (updated) { userRepository.save(customer); System.out.println("Customer user normalized"); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 20773f86..21d7f6d4 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -40,6 +40,7 @@ spring: flyway: enabled: ${FLYWAY_ENABLED:false} + validate-on-migrate: false server: port: ${SERVER_PORT:8080} diff --git a/backend/src/main/resources/db/migration/V1__target_baseline.sql b/backend/src/main/resources/db/migration/V1__target_baseline.sql index bc064913..6f03cf58 100644 --- a/backend/src/main/resources/db/migration/V1__target_baseline.sql +++ b/backend/src/main/resources/db/migration/V1__target_baseline.sql @@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS users ( firstName VARCHAR(50) NOT NULL, lastName VARCHAR(50) NOT NULL, fullName VARCHAR(100) NULL, - phone VARCHAR(20) NULL, + phone VARCHAR(20) NULL UNIQUE, avatarUrl VARCHAR(255) NULL, role VARCHAR(20) NOT NULL, staffRole VARCHAR(50) NULL, @@ -108,7 +108,6 @@ CREATE TABLE IF NOT EXISTS purchaseOrder ( supId BIGINT NOT NULL, storeId BIGINT NOT NULL, orderDate DATE NOT NULL, - status VARCHAR(50) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_purchase_order_supplier FOREIGN KEY (supId) REFERENCES supplier(supId), @@ -150,7 +149,7 @@ CREATE TABLE IF NOT EXISTS pet ( CREATE TABLE IF NOT EXISTS appointment ( appointmentId BIGINT AUTO_INCREMENT PRIMARY KEY, serviceId BIGINT NOT NULL, - petId BIGINT NOT NULL, + petId BIGINT NULL, customerId BIGINT NOT NULL, storeId BIGINT NOT NULL, employeeId BIGINT NOT NULL, @@ -160,7 +159,7 @@ CREATE TABLE IF NOT EXISTS appointment ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_appointment_service FOREIGN KEY (serviceId) REFERENCES service(serviceId), - CONSTRAINT fk_appointment_pet FOREIGN KEY (petId) REFERENCES pet(petId), + CONSTRAINT fk_appointment_pet FOREIGN KEY (petId) REFERENCES pet(petId) ON DELETE SET NULL, CONSTRAINT fk_appointment_customer FOREIGN KEY (customerId) REFERENCES users(id), CONSTRAINT fk_appointment_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), CONSTRAINT fk_appointment_employee FOREIGN KEY (employeeId) REFERENCES users(id) diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql index 88d3835b..befa56fa 100644 --- a/backend/src/main/resources/db/migration/V2__seed_data.sql +++ b/backend/src/main/resources/db/migration/V2__seed_data.sql @@ -204,11 +204,14 @@ INSERT INTO service_species (serviceId, species) VALUES (2, 'Rabbit'), (2, 'Guinea Pig'), (2, 'Hamster'), -(2, 'Bird'), +(2, 'Reptile'), +(2, 'Other'), (3, 'Dog'), (3, 'Cat'), (3, 'Rabbit'), (3, 'Guinea Pig'), +(3, 'Reptile'), +(3, 'Other'), (4, 'Dog'), (4, 'Cat'), (4, 'Rabbit'), @@ -216,11 +219,18 @@ INSERT INTO service_species (serviceId, species) VALUES (4, 'Fish'), (4, 'Hamster'), (4, 'Guinea Pig'), +(4, 'Reptile'), +(4, 'Other'), (5, 'Dog'), (5, 'Cat'), (5, 'Rabbit'), (5, 'Guinea Pig'), (5, 'Hamster'), +(5, 'Reptile'), +(5, 'Other'), +(1, 'Guinea Pig'), +(1, 'Hamster'), +(1, 'Other'), (6, 'Bird'), (7, 'Bird'), (8, 'Fish'); @@ -2247,3 +2257,234 @@ WHERE imageUrl LIKE 'https://images.petshop.local/products/%'; UPDATE storeLocation SET imageUrl = REPLACE(imageUrl, 'https://images.petshop.local/stores/', '/stores/') WHERE imageUrl LIKE 'https://images.petshop.local/stores/%'; + +INSERT IGNORE INTO appointment (appointmentId, serviceId, petId, customerId, storeId, employeeId, appointmentDate, appointmentTime, appointmentStatus) VALUES +(91, 1, NULL, 16, 1, 3, '2026-03-08', '09:00:00', 'COMPLETED'), +(92, 2, NULL, 17, 2, 8, '2026-03-10', '10:30:00', 'COMPLETED'), +(93, 3, NULL, 18, 3, 13, '2026-03-12', '13:00:00', 'MISSED'), +(94, 4, NULL, 19, 1, 6, '2026-03-14', '14:30:00', 'COMPLETED'), +(95, 5, NULL, 20, 2, 7, '2026-03-16', '09:00:00', 'COMPLETED'), +(96, 6, NULL, 21, 3, 12, '2026-03-18', '10:30:00', 'COMPLETED'), +(97, 7, NULL, 22, 1, 5, '2026-03-20', '13:00:00', 'CANCELLED'), +(98, 8, NULL, 23, 2, 10, '2026-03-22', '14:30:00', 'COMPLETED'), +(99, 1, NULL, 24, 3, 11, '2026-03-24', '09:00:00', 'COMPLETED'), +(100, 2, NULL, 25, 1, 4, '2026-03-26', '10:30:00', 'MISSED'), +(101, 3, NULL, 26, 2, 9, '2026-03-28', '13:00:00', 'COMPLETED'), +(102, 4, NULL, 27, 3, 14, '2026-03-30', '14:30:00', 'COMPLETED'), +(103, 5, NULL, 28, 1, 3, '2026-04-01', '09:00:00', 'COMPLETED'), +(104, 6, NULL, 29, 2, 8, '2026-04-03', '10:30:00', 'COMPLETED'), +(105, 7, NULL, 30, 3, 13, '2026-04-05', '13:00:00', 'MISSED'), +(106, 8, NULL, 31, 1, 6, '2026-04-07', '14:30:00', 'COMPLETED'), +(107, 1, NULL, 32, 2, 7, '2026-04-09', '09:00:00', 'COMPLETED'), +(108, 2, NULL, 33, 3, 12, '2026-04-11', '10:30:00', 'CANCELLED'), +(109, 3, NULL, 34, 1, 5, '2026-04-13', '13:00:00', 'COMPLETED'), +(110, 4, NULL, 35, 2, 10, '2026-04-15', '10:00:00', 'SCHEDULED'), +(111, 5, NULL, 36, 3, 11, '2026-04-16', '14:00:00', 'SCHEDULED'); + +INSERT IGNORE INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES +(111, '2026-03-08 09:15:00', 87.50, 'Cash', 3, 1, 3, 0, NULL, 'IN_STORE', NULL, NULL, 87.50, 0.00, 0.00, 8), +(112, '2026-03-09 10:22:00', 145.20, 'Card', 8, 2, 4, 0, NULL, 'IN_STORE', NULL, NULL, 145.20, 0.00, 0.00, 14), +(113, '2026-03-10 11:33:00', 63.75, 'Cash', 13, 3, 5, 0, NULL, 'IN_STORE', NULL, NULL, 63.75, 0.00, 0.00, 6), +(114, '2026-03-11 12:44:00', 210.00, 'Card', 6, 1, 6, 0, NULL, 'ONLINE', NULL, NULL, 210.00, 0.00, 0.00, 21), +(115, '2026-03-12 13:55:00', 38.90, 'Cash', 7, 2, 7, 0, NULL, 'IN_STORE', NULL, NULL, 38.90, 0.00, 0.00, 3), +(116, '2026-03-14 09:10:00', 325.40, 'Card', 12, 3, 8, 0, NULL, 'ONLINE', NULL, NULL, 325.40, 0.00, 0.00, 32), +(117, '2026-03-16 10:25:00', 72.15, 'Cash', 5, 1, 9, 0, NULL, 'IN_STORE', NULL, NULL, 72.15, 0.00, 0.00, 7), +(118, '2026-03-18 11:40:00', 190.80, 'Card', 10, 2, 10, 0, NULL, 'ONLINE', NULL, NULL, 190.80, 0.00, 0.00, 19), +(119, '2026-03-20 12:55:00', 55.30, 'Cash', 11, 3, 11, 0, NULL, 'IN_STORE', NULL, NULL, 55.30, 0.00, 0.00, 5), +(120, '2026-03-22 14:10:00', 412.60, 'Card', 4, 1, 12, 0, NULL, 'ONLINE', NULL, NULL, 412.60, 0.00, 0.00, 41), +(121, '2026-03-24 09:30:00', 98.45, 'Cash', 9, 2, 13, 0, NULL, 'IN_STORE', NULL, NULL, 98.45, 0.00, 0.00, 9), +(122, '2026-03-26 10:45:00', 167.70, 'Card', 14, 3, 14, 0, NULL, 'ONLINE', NULL, NULL, 167.70, 0.00, 0.00, 16), +(123, '2026-03-28 12:00:00', 44.20, 'Cash', 3, 1, 15, 0, NULL, 'IN_STORE', NULL, NULL, 44.20, 0.00, 0.00, 4), +(124, '2026-03-30 13:15:00', 289.55, 'Card', 8, 2, 16, 0, NULL, 'ONLINE', NULL, NULL, 289.55, 0.00, 0.00, 28), +(125, '2026-04-01 09:20:00', 76.80, 'Cash', 13, 3, 17, 0, NULL, 'IN_STORE', NULL, NULL, 76.80, 0.00, 0.00, 7), +(126, '2026-04-03 10:35:00', 234.10, 'Card', 6, 1, 18, 0, NULL, 'ONLINE', NULL, NULL, 234.10, 0.00, 0.00, 23), +(127, '2026-04-05 11:50:00', 52.40, 'Cash', 7, 2, 19, 0, NULL, 'IN_STORE', NULL, NULL, 52.40, 0.00, 0.00, 5), +(128, '2026-04-07 13:05:00', 178.90, 'Card', 12, 3, 20, 0, NULL, 'ONLINE', NULL, NULL, 178.90, 0.00, 0.00, 17), +(129, '2026-04-09 09:15:00', 115.60, 'Cash', 5, 1, 21, 0, NULL, 'IN_STORE', NULL, NULL, 115.60, 0.00, 0.00, 11), +(130, '2026-04-11 10:30:00', 367.25, 'Card', 10, 2, 22, 0, NULL, 'ONLINE', NULL, NULL, 367.25, 0.00, 0.00, 36), +(131, '2026-04-14 11:45:00', 89.70, 'Cash', 11, 3, 23, 0, NULL, 'IN_STORE', NULL, NULL, 89.70, 0.00, 0.00, 8), +(132, '2026-04-15 09:00:00', 145.30, 'Card', 4, 1, 24, 0, NULL, 'ONLINE', NULL, NULL, 145.30, 0.00, 0.00, 14), +(133, '2026-04-16 10:00:00', 78.60, 'Cash', 9, 2, 25, 0, NULL, 'IN_STORE', NULL, NULL, 78.60, 0.00, 0.00, 7); + +INSERT IGNORE INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VALUES +(226, 111, 5, 2, 25.50), +(227, 111, 18, 1, 36.50), +(228, 112, 22, 3, 29.80), +(229, 112, 7, 1, 55.80), +(230, 113, 11, 1, 43.75), +(231, 113, 3, 1, 20.00), +(232, 114, 15, 4, 40.00), +(233, 114, 29, 1, 50.00), +(234, 115, 8, 1, 38.90), +(235, 116, 20, 2, 95.00), +(236, 116, 33, 1, 135.40), +(237, 117, 6, 1, 72.15), +(238, 118, 14, 3, 45.00), +(239, 118, 25, 1, 55.80), +(240, 119, 9, 1, 55.30), +(241, 120, 17, 2, 125.00), +(242, 120, 31, 1, 162.60), +(243, 121, 2, 2, 35.50), +(244, 121, 10, 1, 27.45), +(245, 122, 23, 2, 58.50), +(246, 122, 36, 1, 50.70), +(247, 123, 4, 1, 44.20), +(248, 124, 19, 3, 65.00), +(249, 124, 28, 1, 94.55), +(250, 125, 12, 1, 76.80), +(251, 126, 16, 2, 80.00), +(252, 126, 27, 1, 74.10), +(253, 127, 7, 1, 52.40), +(254, 128, 21, 2, 65.00), +(255, 128, 32, 1, 48.90), +(256, 129, 13, 2, 47.80), +(257, 129, 1, 1, 20.00), +(258, 130, 24, 3, 80.00), +(259, 130, 37, 1, 127.25), +(260, 131, 6, 1, 89.70), +(261, 132, 15, 2, 55.00), +(262, 132, 29, 1, 35.30), +(263, 133, 8, 1, 78.60); + +INSERT INTO activityLog (userId, storeId, usernameSnapshot, fullNameSnapshot, roleSnapshot, storeNameSnapshot, activity, logTimestamp) VALUES +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 08:02:11'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new pet | POST /api/v1/pets → 201', '2026-01-15 08:15:44'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new product | POST /api/v1/products → 201', '2026-01-15 08:31:07'), +(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 09:00:00'), +(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 09:04:22'), +(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 10:12:33'), +(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-01-15 10:18:05'), +(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-01-15 10:25:50'), +(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Updated appointment #12 | PUT /api/v1/appointments/12 → 200', '2026-01-16 11:05:30'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-17 07:58:44'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated pet #8 | PUT /api/v1/pets/8 → 200', '2026-01-17 08:10:19'), +(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-20 09:01:55'), +(16, NULL,'alex.brown', 'Alex Brown', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-01-20 14:30:22'), +(16, NULL,'alex.brown', 'Alex Brown', 'CUSTOMER', NULL, 'Submitted an adoption request | POST /api/v1/adoptions/request → 201', '2026-01-20 14:45:08'), +(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-22 08:55:00'), +(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Updated pet #14 | PUT /api/v1/pets/14 → 200', '2026-01-22 09:20:17'), +(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-25 08:00:00'), +(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Created a new service | POST /api/v1/services → 201', '2026-01-25 08:22:41'), +(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Created a new employee | POST /api/v1/employees → 201', '2026-01-25 09:05:14'), +(17, NULL,'alex.clark', 'Alex Clark', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-01-28 18:30:00'), +(17, NULL,'alex.clark', 'Alex Clark', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-01-28 18:35:12'), +(17, NULL,'alex.clark', 'Alex Clark', 'CUSTOMER', NULL, 'Updated their profile | PUT /api/v1/auth/me → 200', '2026-01-28 18:40:55'), +(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-03 09:00:00'), +(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-02-03 10:15:38'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-05 07:50:00'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Deleted pet #3 | DELETE /api/v1/pets/3 → 200', '2026-02-05 08:05:33'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated product #22 | PUT /api/v1/products/22 → 200', '2026-02-05 08:30:44'), +(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-02-07 12:00:00'), +(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-02-07 12:08:17'), +(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Applied a coupon to cart | POST /api/v1/cart/apply-coupon → 200', '2026-02-07 12:12:05'), +(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-02-07 12:20:30'), +(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-10 09:00:00'), +(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Created a new appointment | POST /api/v1/appointments → 201', '2026-02-10 09:30:22'), +(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Updated appointment #25 | PUT /api/v1/appointments/25 → 200', '2026-02-10 11:45:09'), +(19, NULL,'alex.martinez', 'Alex Martinez', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-02-14 15:00:00'), +(19, NULL,'alex.martinez', 'Alex Martinez', 'CUSTOMER', NULL, 'Started a new chat conversation | POST /api/v1/chat/conversations → 201','2026-02-14 15:05:44'), +(19, NULL,'alex.martinez', 'Alex Martinez', 'CUSTOMER', NULL, 'Sent a chat message | POST /api/v1/chat/conversations/5/messages → 201', '2026-02-14 15:08:22'), +(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-17 09:00:00'), +(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Uploaded image for pet #31 | POST /api/v1/pets/31/image → 200', '2026-02-17 09:25:11'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-20 08:00:00'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new pet | POST /api/v1/pets → 201', '2026-02-20 08:20:35'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated user #18 | PUT /api/v1/users/18 → 200', '2026-02-20 09:10:00'), +(20, NULL,'alex.anderson', 'Alex Anderson', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-02-22 19:00:00'), +(20, NULL,'alex.anderson', 'Alex Anderson', 'CUSTOMER', NULL, 'Submitted an adoption request | POST /api/v1/adoptions/request → 201', '2026-02-22 19:15:40'), +(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-01 09:00:00'), +(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-01 10:30:15'), +(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Updated appointment #33 | PUT /api/v1/appointments/33 → 200', '2026-03-01 14:05:22'), +(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-03-05 11:00:00'), +(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-03-05 11:10:30'), +(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Started checkout | POST /api/v1/cart/checkout → 200', '2026-03-05 11:18:44'), +(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-05 11:22:17'), +(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-08 08:00:00'), +(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Deleted multiple pets | POST /api/v1/pets/bulk-delete → 200', '2026-03-08 08:30:00'), +(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-10 09:00:00'), +(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-10 10:45:33'), +(22, NULL,'alex.parker', 'Alex Parker', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-03-12 16:00:00'), +(22, NULL,'alex.parker', 'Alex Parker', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-03-12 16:05:22'), +(22, NULL,'alex.parker', 'Alex Parker', 'CUSTOMER', NULL, 'Updated their profile | PUT /api/v1/auth/me → 200', '2026-03-12 16:20:00'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-15 07:55:00'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new product | POST /api/v1/products → 201', '2026-03-15 08:10:45'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated product #35 | PUT /api/v1/products/35 → 200', '2026-03-15 08:40:12'), +(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-18 09:00:00'), +(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Updated appointment #45 | PUT /api/v1/appointments/45 → 200', '2026-03-18 09:35:08'), +(23, NULL,'alex.evans', 'Alex Evans', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-03-20 20:00:00'), +(23, NULL,'alex.evans', 'Alex Evans', 'CUSTOMER', NULL, 'Started a new chat conversation | POST /api/v1/chat/conversations → 201','2026-03-20 20:05:30'), +(23, NULL,'alex.evans', 'Alex Evans', 'CUSTOMER', NULL, 'Sent a chat message | POST /api/v1/chat/conversations/9/messages → 201', '2026-03-20 20:08:11'), +(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-24 09:00:00'), +(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Created a new appointment | POST /api/v1/appointments → 201', '2026-03-24 09:20:44'), +(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-27 09:00:00'), +(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Updated appointment #52 | PUT /api/v1/appointments/52 → 200', '2026-03-27 10:10:19'), +(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-27 11:30:00'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-01 08:00:00'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new pet | POST /api/v1/pets → 201', '2026-04-01 08:15:00'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated user #25 | PUT /api/v1/users/25 → 200', '2026-04-01 09:00:22'), +(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-05 10:00:00'), +(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-04-05 10:15:00'), +(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-04-05 10:25:44'), +(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-07 09:00:00'), +(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Updated pet #47 | PUT /api/v1/pets/47 → 200', '2026-04-07 09:40:55'), +(24, NULL,'alex.scott', 'Alex Scott', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-09 17:00:00'), +(24, NULL,'alex.scott', 'Alex Scott', 'CUSTOMER', NULL, 'Submitted an adoption request | POST /api/v1/adoptions/request → 201', '2026-04-09 17:20:33'), +(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-12 08:00:00'), +(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Created a new service | POST /api/v1/services → 201', '2026-04-12 08:25:18'), +(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-14 09:00:00'), +(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-04-14 10:55:30'), +(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Updated appointment #60 | PUT /api/v1/appointments/60 → 200', '2026-04-14 14:20:00'), +(25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-15 11:00:00'), +(25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-04-15 11:10:22'), +(25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-04-15 11:30:00'); + +INSERT IGNORE INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES +(134, '2026-04-10 09:15:00', 57.67, 'Cash', 4, 1, 16, 0, NULL, 'IN_STORE', NULL, NULL, 57.67, 0.00, 0.00, 5), +(135, '2026-04-10 11:30:00', 143.55, 'Card', 8, 2, 17, 0, NULL, 'ONLINE', NULL, NULL, 143.55, 0.00, 0.00, 14), +(136, '2026-04-10 14:45:00', 50.09, 'Cash', 12, 3, 18, 0, NULL, 'IN_STORE', NULL, NULL, 50.09, 0.00, 0.00, 5), +(137, '2026-04-11 10:00:00', 114.48, 'Card', 5, 1, 19, 0, NULL, 'ONLINE', NULL, NULL, 114.48, 0.00, 0.00, 11), +(138, '2026-04-11 13:20:00', 93.55, 'Cash', 9, 2, 20, 0, NULL, 'IN_STORE', NULL, NULL, 93.55, 0.00, 0.00, 9), +(139, '2026-04-12 09:45:00', 100.71, 'Card', 13, 3, 21, 0, NULL, 'ONLINE', NULL, NULL, 100.71, 0.00, 0.00, 10), +(140, '2026-04-12 11:00:00', 51.07, 'Cash', 6, 1, 22, 0, NULL, 'IN_STORE', NULL, NULL, 51.07, 0.00, 0.00, 5), +(141, '2026-04-12 15:30:00', 139.66, 'Card', 7, 2, 23, 0, NULL, 'ONLINE', NULL, NULL, 139.66, 0.00, 0.00, 13), +(142, '2026-04-13 09:00:00', 73.98, 'Cash', 14, 3, 24, 0, NULL, 'IN_STORE', NULL, NULL, 73.98, 0.00, 0.00, 7), +(143, '2026-04-13 12:15:00', 134.76, 'Card', 4, 1, 25, 0, NULL, 'ONLINE', NULL, NULL, 134.76, 0.00, 0.00, 13), +(144, '2026-04-14 10:30:00', 80.40, 'Cash', 10, 2, 26, 0, NULL, 'IN_STORE', NULL, NULL, 80.40, 0.00, 0.00, 8), +(145, '2026-04-14 14:00:00', 125.90, 'Card', 11, 3, 27, 0, NULL, 'ONLINE', NULL, NULL, 125.90, 0.00, 0.00, 12), +(146, '2026-04-15 10:45:00', 80.62, 'Cash', 5, 1, 28, 0, NULL, 'IN_STORE', NULL, NULL, 80.62, 0.00, 0.00, 8), +(147, '2026-04-15 13:00:00', 141.28, 'Card', 8, 2, 29, 0, NULL, 'ONLINE', NULL, NULL, 141.28, 0.00, 0.00, 14), +(148, '2026-04-16 09:30:00', 97.85, 'Cash', 12, 3, 30, 0, NULL, 'IN_STORE', NULL, NULL, 97.85, 0.00, 0.00, 9), +(149, '2026-04-16 11:45:00', 89.36, 'Card', 6, 1, 31, 0, NULL, 'ONLINE', NULL, NULL, 89.36, 0.00, 0.00, 8); + +INSERT IGNORE INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VALUES +(264, 134, 1, 2, 25.09), +(265, 134, 11, 1, 7.49), +(266, 135, 7, 2, 57.62), +(267, 135, 25, 1, 28.31), +(268, 136, 3, 1, 35.93), +(269, 136, 14, 1, 14.16), +(270, 137, 15, 3, 16.38), +(271, 137, 26, 2, 32.67), +(272, 138, 8, 1, 63.05), +(273, 138, 22, 2, 15.25), +(274, 139, 20, 2, 27.49), +(275, 139, 29, 1, 45.73), +(276, 140, 4, 1, 41.36), +(277, 140, 12, 1, 9.71), +(278, 141, 34, 2, 59.42), +(279, 141, 17, 1, 20.82), +(280, 142, 6, 1, 52.20), +(281, 142, 21, 2, 10.89), +(282, 143, 37, 1, 97.56), +(283, 143, 16, 2, 18.60), +(284, 144, 9, 1, 68.47), +(285, 144, 13, 1, 11.93), +(286, 145, 19, 3, 25.27), +(287, 145, 30, 1, 50.09), +(288, 146, 2, 2, 30.51), +(289, 146, 23, 1, 19.60), +(290, 147, 35, 1, 72.13), +(291, 147, 18, 3, 23.05), +(292, 148, 10, 1, 73.89), +(293, 148, 24, 1, 23.96), +(294, 149, 31, 2, 21.29), +(295, 149, 5, 1, 46.78); diff --git a/backend/src/main/resources/db/migration/V3__nullable_appointment_petid.sql b/backend/src/main/resources/db/migration/V3__nullable_appointment_petid.sql deleted file mode 100644 index 06639401..00000000 --- a/backend/src/main/resources/db/migration/V3__nullable_appointment_petid.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE appointment MODIFY petId BIGINT NULL; -ALTER TABLE appointment DROP FOREIGN KEY fk_appointment_pet; -ALTER TABLE appointment ADD CONSTRAINT fk_appointment_pet FOREIGN KEY (petId) REFERENCES pet(petId) ON DELETE SET NULL; diff --git a/backend/src/main/resources/db/migration/V4__drop_purchase_order_status.sql b/backend/src/main/resources/db/migration/V4__drop_purchase_order_status.sql deleted file mode 100644 index bef741cf..00000000 --- a/backend/src/main/resources/db/migration/V4__drop_purchase_order_status.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE purchaseOrder DROP COLUMN status; diff --git a/backend/src/main/resources/db/migration/V5__seed_data_2026.sql b/backend/src/main/resources/db/migration/V5__seed_data_2026.sql deleted file mode 100644 index 3f8f0a40..00000000 --- a/backend/src/main/resources/db/migration/V5__seed_data_2026.sql +++ /dev/null @@ -1,87 +0,0 @@ -INSERT IGNORE INTO appointment (appointmentId, serviceId, petId, customerId, storeId, employeeId, appointmentDate, appointmentTime, appointmentStatus) VALUES -(91, 1, NULL, 16, 1, 3, '2026-03-08', '09:00:00', 'COMPLETED'), -(92, 2, NULL, 17, 2, 8, '2026-03-10', '10:30:00', 'COMPLETED'), -(93, 3, NULL, 18, 3, 13, '2026-03-12', '13:00:00', 'MISSED'), -(94, 4, NULL, 19, 1, 6, '2026-03-14', '14:30:00', 'COMPLETED'), -(95, 5, NULL, 20, 2, 7, '2026-03-16', '09:00:00', 'COMPLETED'), -(96, 6, NULL, 21, 3, 12, '2026-03-18', '10:30:00', 'COMPLETED'), -(97, 7, NULL, 22, 1, 5, '2026-03-20', '13:00:00', 'CANCELLED'), -(98, 8, NULL, 23, 2, 10, '2026-03-22', '14:30:00', 'COMPLETED'), -(99, 1, NULL, 24, 3, 11, '2026-03-24', '09:00:00', 'COMPLETED'), -(100, 2, NULL, 25, 1, 4, '2026-03-26', '10:30:00', 'MISSED'), -(101, 3, NULL, 26, 2, 9, '2026-03-28', '13:00:00', 'COMPLETED'), -(102, 4, NULL, 27, 3, 14, '2026-03-30', '14:30:00', 'COMPLETED'), -(103, 5, NULL, 28, 1, 3, '2026-04-01', '09:00:00', 'COMPLETED'), -(104, 6, NULL, 29, 2, 8, '2026-04-03', '10:30:00', 'COMPLETED'), -(105, 7, NULL, 30, 3, 13, '2026-04-05', '13:00:00', 'MISSED'), -(106, 8, NULL, 31, 1, 6, '2026-04-07', '14:30:00', 'COMPLETED'), -(107, 1, NULL, 32, 2, 7, '2026-04-09', '09:00:00', 'COMPLETED'), -(108, 2, NULL, 33, 3, 12, '2026-04-11', '10:30:00', 'CANCELLED'), -(109, 3, NULL, 34, 1, 5, '2026-04-13', '13:00:00', 'COMPLETED'), -(110, 4, NULL, 35, 2, 10, '2026-04-15', '10:00:00', 'SCHEDULED'), -(111, 5, NULL, 36, 3, 11, '2026-04-16', '14:00:00', 'SCHEDULED'); - -INSERT IGNORE INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES -(111, '2026-03-08 09:15:00', 87.50, 'Cash', 3, 1, 3, 0, NULL, 'IN_STORE', NULL, NULL, 87.50, 0.00, 0.00, 8), -(112, '2026-03-09 10:22:00', 145.20, 'Card', 8, 2, 4, 0, NULL, 'IN_STORE', NULL, NULL, 145.20, 0.00, 0.00, 14), -(113, '2026-03-10 11:33:00', 63.75, 'Cash', 13, 3, 5, 0, NULL, 'IN_STORE', NULL, NULL, 63.75, 0.00, 0.00, 6), -(114, '2026-03-11 12:44:00', 210.00, 'Card', 6, 1, 6, 0, NULL, 'ONLINE', NULL, NULL, 210.00, 0.00, 0.00, 21), -(115, '2026-03-12 13:55:00', 38.90, 'Cash', 7, 2, 7, 0, NULL, 'IN_STORE', NULL, NULL, 38.90, 0.00, 0.00, 3), -(116, '2026-03-14 09:10:00', 325.40, 'Card', 12, 3, 8, 0, NULL, 'ONLINE', NULL, NULL, 325.40, 0.00, 0.00, 32), -(117, '2026-03-16 10:25:00', 72.15, 'Cash', 5, 1, 9, 0, NULL, 'IN_STORE', NULL, NULL, 72.15, 0.00, 0.00, 7), -(118, '2026-03-18 11:40:00', 190.80, 'Card', 10, 2, 10, 0, NULL, 'ONLINE', NULL, NULL, 190.80, 0.00, 0.00, 19), -(119, '2026-03-20 12:55:00', 55.30, 'Cash', 11, 3, 11, 0, NULL, 'IN_STORE', NULL, NULL, 55.30, 0.00, 0.00, 5), -(120, '2026-03-22 14:10:00', 412.60, 'Card', 4, 1, 12, 0, NULL, 'ONLINE', NULL, NULL, 412.60, 0.00, 0.00, 41), -(121, '2026-03-24 09:30:00', 98.45, 'Cash', 9, 2, 13, 0, NULL, 'IN_STORE', NULL, NULL, 98.45, 0.00, 0.00, 9), -(122, '2026-03-26 10:45:00', 167.70, 'Card', 14, 3, 14, 0, NULL, 'ONLINE', NULL, NULL, 167.70, 0.00, 0.00, 16), -(123, '2026-03-28 12:00:00', 44.20, 'Cash', 3, 1, 15, 0, NULL, 'IN_STORE', NULL, NULL, 44.20, 0.00, 0.00, 4), -(124, '2026-03-30 13:15:00', 289.55, 'Card', 8, 2, 16, 0, NULL, 'ONLINE', NULL, NULL, 289.55, 0.00, 0.00, 28), -(125, '2026-04-01 09:20:00', 76.80, 'Cash', 13, 3, 17, 0, NULL, 'IN_STORE', NULL, NULL, 76.80, 0.00, 0.00, 7), -(126, '2026-04-03 10:35:00', 234.10, 'Card', 6, 1, 18, 0, NULL, 'ONLINE', NULL, NULL, 234.10, 0.00, 0.00, 23), -(127, '2026-04-05 11:50:00', 52.40, 'Cash', 7, 2, 19, 0, NULL, 'IN_STORE', NULL, NULL, 52.40, 0.00, 0.00, 5), -(128, '2026-04-07 13:05:00', 178.90, 'Card', 12, 3, 20, 0, NULL, 'ONLINE', NULL, NULL, 178.90, 0.00, 0.00, 17), -(129, '2026-04-09 09:15:00', 115.60, 'Cash', 5, 1, 21, 0, NULL, 'IN_STORE', NULL, NULL, 115.60, 0.00, 0.00, 11), -(130, '2026-04-11 10:30:00', 367.25, 'Card', 10, 2, 22, 0, NULL, 'ONLINE', NULL, NULL, 367.25, 0.00, 0.00, 36), -(131, '2026-04-14 11:45:00', 89.70, 'Cash', 11, 3, 23, 0, NULL, 'IN_STORE', NULL, NULL, 89.70, 0.00, 0.00, 8), -(132, '2026-04-15 09:00:00', 145.30, 'Card', 4, 1, 24, 0, NULL, 'ONLINE', NULL, NULL, 145.30, 0.00, 0.00, 14), -(133, '2026-04-16 10:00:00', 78.60, 'Cash', 9, 2, 25, 0, NULL, 'IN_STORE', NULL, NULL, 78.60, 0.00, 0.00, 7); - -INSERT IGNORE INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VALUES -(226, 111, 5, 2, 25.50), -(227, 111, 18, 1, 36.50), -(228, 112, 22, 3, 29.80), -(229, 112, 7, 1, 55.80), -(230, 113, 11, 1, 43.75), -(231, 113, 3, 1, 20.00), -(232, 114, 15, 4, 40.00), -(233, 114, 29, 1, 50.00), -(234, 115, 8, 1, 38.90), -(235, 116, 20, 2, 95.00), -(236, 116, 33, 1, 135.40), -(237, 117, 6, 1, 72.15), -(238, 118, 14, 3, 45.00), -(239, 118, 25, 1, 55.80), -(240, 119, 9, 1, 55.30), -(241, 120, 17, 2, 125.00), -(242, 120, 31, 1, 162.60), -(243, 121, 2, 2, 35.50), -(244, 121, 10, 1, 27.45), -(245, 122, 23, 2, 58.50), -(246, 122, 36, 1, 50.70), -(247, 123, 4, 1, 44.20), -(248, 124, 19, 3, 65.00), -(249, 124, 28, 1, 94.55), -(250, 125, 12, 1, 76.80), -(251, 126, 16, 2, 80.00), -(252, 126, 27, 1, 74.10), -(253, 127, 7, 1, 52.40), -(254, 128, 21, 2, 65.00), -(255, 128, 32, 1, 48.90), -(256, 129, 13, 2, 47.80), -(257, 129, 1, 1, 20.00), -(258, 130, 24, 3, 80.00), -(259, 130, 37, 1, 127.25), -(260, 131, 6, 1, 89.70), -(261, 132, 15, 2, 55.00), -(262, 132, 29, 1, 35.30), -(263, 133, 8, 1, 78.60); diff --git a/backend/src/main/resources/db/migration/V6__unique_constraints.sql b/backend/src/main/resources/db/migration/V6__unique_constraints.sql deleted file mode 100644 index 5e26b311..00000000 --- a/backend/src/main/resources/db/migration/V6__unique_constraints.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE users ADD CONSTRAINT uq_users_phone UNIQUE (phone); diff --git a/backend/src/main/resources/db/migration/V7__fix_service_species.sql b/backend/src/main/resources/db/migration/V7__fix_service_species.sql deleted file mode 100644 index 4c22a1b0..00000000 --- a/backend/src/main/resources/db/migration/V7__fix_service_species.sql +++ /dev/null @@ -1,14 +0,0 @@ -DELETE FROM service_species WHERE serviceId = 2 AND species = 'Bird'; - -INSERT INTO service_species (serviceId, species) VALUES -(1, 'Guinea Pig'), -(1, 'Hamster'), -(1, 'Other'), -(2, 'Reptile'), -(2, 'Other'), -(3, 'Reptile'), -(3, 'Other'), -(4, 'Reptile'), -(4, 'Other'), -(5, 'Reptile'), -(5, 'Other'); diff --git a/backend/src/main/resources/db/migration/V8__seed_activity_logs.sql b/backend/src/main/resources/db/migration/V8__seed_activity_logs.sql deleted file mode 100644 index aa3fa6d1..00000000 --- a/backend/src/main/resources/db/migration/V8__seed_activity_logs.sql +++ /dev/null @@ -1,90 +0,0 @@ -INSERT INTO activityLog (userId, storeId, usernameSnapshot, fullNameSnapshot, roleSnapshot, storeNameSnapshot, activity, logTimestamp) VALUES -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 08:02:11'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new pet | POST /api/v1/pets → 201', '2026-01-15 08:15:44'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new product | POST /api/v1/products → 201', '2026-01-15 08:31:07'), -(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 09:00:00'), -(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 09:04:22'), -(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 10:12:33'), -(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-01-15 10:18:05'), -(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-01-15 10:25:50'), -(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Updated appointment #12 | PUT /api/v1/appointments/12 → 200', '2026-01-16 11:05:30'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-17 07:58:44'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated pet #8 | PUT /api/v1/pets/8 → 200', '2026-01-17 08:10:19'), -(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-20 09:01:55'), -(16, NULL,'alex.brown', 'Alex Brown', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-01-20 14:30:22'), -(16, NULL,'alex.brown', 'Alex Brown', 'CUSTOMER', NULL, 'Submitted an adoption request | POST /api/v1/adoptions/request → 201', '2026-01-20 14:45:08'), -(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-22 08:55:00'), -(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Updated pet #14 | PUT /api/v1/pets/14 → 200', '2026-01-22 09:20:17'), -(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-25 08:00:00'), -(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Created a new service | POST /api/v1/services → 201', '2026-01-25 08:22:41'), -(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Created a new employee | POST /api/v1/employees → 201', '2026-01-25 09:05:14'), -(17, NULL,'alex.clark', 'Alex Clark', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-01-28 18:30:00'), -(17, NULL,'alex.clark', 'Alex Clark', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-01-28 18:35:12'), -(17, NULL,'alex.clark', 'Alex Clark', 'CUSTOMER', NULL, 'Updated their profile | PUT /api/v1/auth/me → 200', '2026-01-28 18:40:55'), -(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-03 09:00:00'), -(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-02-03 10:15:38'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-05 07:50:00'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Deleted pet #3 | DELETE /api/v1/pets/3 → 200', '2026-02-05 08:05:33'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated product #22 | PUT /api/v1/products/22 → 200', '2026-02-05 08:30:44'), -(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-02-07 12:00:00'), -(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-02-07 12:08:17'), -(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Applied a coupon to cart | POST /api/v1/cart/apply-coupon → 200', '2026-02-07 12:12:05'), -(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-02-07 12:20:30'), -(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-10 09:00:00'), -(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Created a new appointment | POST /api/v1/appointments → 201', '2026-02-10 09:30:22'), -(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Updated appointment #25 | PUT /api/v1/appointments/25 → 200', '2026-02-10 11:45:09'), -(19, NULL,'alex.martinez', 'Alex Martinez', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-02-14 15:00:00'), -(19, NULL,'alex.martinez', 'Alex Martinez', 'CUSTOMER', NULL, 'Started a new chat conversation | POST /api/v1/chat/conversations → 201','2026-02-14 15:05:44'), -(19, NULL,'alex.martinez', 'Alex Martinez', 'CUSTOMER', NULL, 'Sent a chat message | POST /api/v1/chat/conversations/5/messages → 201', '2026-02-14 15:08:22'), -(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-17 09:00:00'), -(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Uploaded image for pet #31 | POST /api/v1/pets/31/image → 200', '2026-02-17 09:25:11'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-20 08:00:00'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new pet | POST /api/v1/pets → 201', '2026-02-20 08:20:35'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated user #18 | PUT /api/v1/users/18 → 200', '2026-02-20 09:10:00'), -(20, NULL,'alex.anderson', 'Alex Anderson', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-02-22 19:00:00'), -(20, NULL,'alex.anderson', 'Alex Anderson', 'CUSTOMER', NULL, 'Submitted an adoption request | POST /api/v1/adoptions/request → 201', '2026-02-22 19:15:40'), -(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-01 09:00:00'), -(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-01 10:30:15'), -(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Updated appointment #33 | PUT /api/v1/appointments/33 → 200', '2026-03-01 14:05:22'), -(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-03-05 11:00:00'), -(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-03-05 11:10:30'), -(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Started checkout | POST /api/v1/cart/checkout → 200', '2026-03-05 11:18:44'), -(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-05 11:22:17'), -(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-08 08:00:00'), -(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Deleted multiple pets | POST /api/v1/pets/bulk-delete → 200', '2026-03-08 08:30:00'), -(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-10 09:00:00'), -(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-10 10:45:33'), -(22, NULL,'alex.parker', 'Alex Parker', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-03-12 16:00:00'), -(22, NULL,'alex.parker', 'Alex Parker', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-03-12 16:05:22'), -(22, NULL,'alex.parker', 'Alex Parker', 'CUSTOMER', NULL, 'Updated their profile | PUT /api/v1/auth/me → 200', '2026-03-12 16:20:00'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-15 07:55:00'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new product | POST /api/v1/products → 201', '2026-03-15 08:10:45'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated product #35 | PUT /api/v1/products/35 → 200', '2026-03-15 08:40:12'), -(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-18 09:00:00'), -(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Updated appointment #45 | PUT /api/v1/appointments/45 → 200', '2026-03-18 09:35:08'), -(23, NULL,'alex.evans', 'Alex Evans', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-03-20 20:00:00'), -(23, NULL,'alex.evans', 'Alex Evans', 'CUSTOMER', NULL, 'Started a new chat conversation | POST /api/v1/chat/conversations → 201','2026-03-20 20:05:30'), -(23, NULL,'alex.evans', 'Alex Evans', 'CUSTOMER', NULL, 'Sent a chat message | POST /api/v1/chat/conversations/9/messages → 201', '2026-03-20 20:08:11'), -(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-24 09:00:00'), -(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Created a new appointment | POST /api/v1/appointments → 201', '2026-03-24 09:20:44'), -(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-27 09:00:00'), -(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Updated appointment #52 | PUT /api/v1/appointments/52 → 200', '2026-03-27 10:10:19'), -(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-27 11:30:00'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-01 08:00:00'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new pet | POST /api/v1/pets → 201', '2026-04-01 08:15:00'), -(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated user #25 | PUT /api/v1/users/25 → 200', '2026-04-01 09:00:22'), -(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-05 10:00:00'), -(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-04-05 10:15:00'), -(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-04-05 10:25:44'), -(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-07 09:00:00'), -(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Updated pet #47 | PUT /api/v1/pets/47 → 200', '2026-04-07 09:40:55'), -(24, NULL,'alex.scott', 'Alex Scott', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-09 17:00:00'), -(24, NULL,'alex.scott', 'Alex Scott', 'CUSTOMER', NULL, 'Submitted an adoption request | POST /api/v1/adoptions/request → 201', '2026-04-09 17:20:33'), -(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-12 08:00:00'), -(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Created a new service | POST /api/v1/services → 201', '2026-04-12 08:25:18'), -(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-14 09:00:00'), -(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-04-14 10:55:30'), -(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Updated appointment #60 | PUT /api/v1/appointments/60 → 200', '2026-04-14 14:20:00'), -(25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-15 11:00:00'), -(25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-04-15 11:10:22'), -(25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-04-15 11:30:00'); diff --git a/backend/src/main/resources/db/migration/V9__seed_recent_sales.sql b/backend/src/main/resources/db/migration/V9__seed_recent_sales.sql deleted file mode 100644 index 23695f6f..00000000 --- a/backend/src/main/resources/db/migration/V9__seed_recent_sales.sql +++ /dev/null @@ -1,51 +0,0 @@ -INSERT IGNORE INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES -(134, '2026-04-10 09:15:00', 57.67, 'Cash', 4, 1, 16, 0, NULL, 'IN_STORE', NULL, NULL, 57.67, 0.00, 0.00, 5), -(135, '2026-04-10 11:30:00', 143.55, 'Card', 8, 2, 17, 0, NULL, 'ONLINE', NULL, NULL, 143.55, 0.00, 0.00, 14), -(136, '2026-04-10 14:45:00', 50.09, 'Cash', 12, 3, 18, 0, NULL, 'IN_STORE', NULL, NULL, 50.09, 0.00, 0.00, 5), -(137, '2026-04-11 10:00:00', 114.48, 'Card', 5, 1, 19, 0, NULL, 'ONLINE', NULL, NULL, 114.48, 0.00, 0.00, 11), -(138, '2026-04-11 13:20:00', 93.55, 'Cash', 9, 2, 20, 0, NULL, 'IN_STORE', NULL, NULL, 93.55, 0.00, 0.00, 9), -(139, '2026-04-12 09:45:00', 100.71, 'Card', 13, 3, 21, 0, NULL, 'ONLINE', NULL, NULL, 100.71, 0.00, 0.00, 10), -(140, '2026-04-12 11:00:00', 51.07, 'Cash', 6, 1, 22, 0, NULL, 'IN_STORE', NULL, NULL, 51.07, 0.00, 0.00, 5), -(141, '2026-04-12 15:30:00', 139.66, 'Card', 7, 2, 23, 0, NULL, 'ONLINE', NULL, NULL, 139.66, 0.00, 0.00, 13), -(142, '2026-04-13 09:00:00', 73.98, 'Cash', 14, 3, 24, 0, NULL, 'IN_STORE', NULL, NULL, 73.98, 0.00, 0.00, 7), -(143, '2026-04-13 12:15:00', 134.76, 'Card', 4, 1, 25, 0, NULL, 'ONLINE', NULL, NULL, 134.76, 0.00, 0.00, 13), -(144, '2026-04-14 10:30:00', 80.40, 'Cash', 10, 2, 26, 0, NULL, 'IN_STORE', NULL, NULL, 80.40, 0.00, 0.00, 8), -(145, '2026-04-14 14:00:00', 125.90, 'Card', 11, 3, 27, 0, NULL, 'ONLINE', NULL, NULL, 125.90, 0.00, 0.00, 12), -(146, '2026-04-15 10:45:00', 80.62, 'Cash', 5, 1, 28, 0, NULL, 'IN_STORE', NULL, NULL, 80.62, 0.00, 0.00, 8), -(147, '2026-04-15 13:00:00', 141.28, 'Card', 8, 2, 29, 0, NULL, 'ONLINE', NULL, NULL, 141.28, 0.00, 0.00, 14), -(148, '2026-04-16 09:30:00', 97.85, 'Cash', 12, 3, 30, 0, NULL, 'IN_STORE', NULL, NULL, 97.85, 0.00, 0.00, 9), -(149, '2026-04-16 11:45:00', 89.36, 'Card', 6, 1, 31, 0, NULL, 'ONLINE', NULL, NULL, 89.36, 0.00, 0.00, 8); - -INSERT IGNORE INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VALUES -(264, 134, 1, 2, 25.09), -(265, 134, 11, 1, 7.49), -(266, 135, 7, 2, 57.62), -(267, 135, 25, 1, 28.31), -(268, 136, 3, 1, 35.93), -(269, 136, 14, 1, 14.16), -(270, 137, 15, 3, 16.38), -(271, 137, 26, 2, 32.67), -(272, 138, 8, 1, 63.05), -(273, 138, 22, 2, 15.25), -(274, 139, 20, 2, 27.49), -(275, 139, 29, 1, 45.73), -(276, 140, 4, 1, 41.36), -(277, 140, 12, 1, 9.71), -(278, 141, 34, 2, 59.42), -(279, 141, 17, 1, 20.82), -(280, 142, 6, 1, 52.20), -(281, 142, 21, 2, 10.89), -(282, 143, 37, 1, 97.56), -(283, 143, 16, 2, 18.60), -(284, 144, 9, 1, 68.47), -(285, 144, 13, 1, 11.93), -(286, 145, 19, 3, 25.27), -(287, 145, 30, 1, 50.09), -(288, 146, 2, 2, 30.51), -(289, 146, 23, 1, 19.60), -(290, 147, 35, 1, 72.13), -(291, 147, 18, 3, 23.05), -(292, 148, 10, 1, 73.89), -(293, 148, 24, 1, 23.96), -(294, 149, 31, 2, 21.29), -(295, 149, 5, 1, 46.78); -- 2.49.1 From 2bbb722693e81eb26654763db475a5293043c79f Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 19 Apr 2026 17:57:22 -0600 Subject: [PATCH 08/34] fix seed data gaps --- .../resources/db/migration/V2__seed_data.sql | 86 +++++++++++++------ 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql index befa56fa..33505a84 100644 --- a/backend/src/main/resources/db/migration/V2__seed_data.sql +++ b/backend/src/main/resources/db/migration/V2__seed_data.sql @@ -2259,27 +2259,27 @@ SET imageUrl = REPLACE(imageUrl, 'https://images.petshop.local/stores/', '/store WHERE imageUrl LIKE 'https://images.petshop.local/stores/%'; INSERT IGNORE INTO appointment (appointmentId, serviceId, petId, customerId, storeId, employeeId, appointmentDate, appointmentTime, appointmentStatus) VALUES -(91, 1, NULL, 16, 1, 3, '2026-03-08', '09:00:00', 'COMPLETED'), -(92, 2, NULL, 17, 2, 8, '2026-03-10', '10:30:00', 'COMPLETED'), -(93, 3, NULL, 18, 3, 13, '2026-03-12', '13:00:00', 'MISSED'), -(94, 4, NULL, 19, 1, 6, '2026-03-14', '14:30:00', 'COMPLETED'), -(95, 5, NULL, 20, 2, 7, '2026-03-16', '09:00:00', 'COMPLETED'), -(96, 6, NULL, 21, 3, 12, '2026-03-18', '10:30:00', 'COMPLETED'), -(97, 7, NULL, 22, 1, 5, '2026-03-20', '13:00:00', 'CANCELLED'), -(98, 8, NULL, 23, 2, 10, '2026-03-22', '14:30:00', 'COMPLETED'), -(99, 1, NULL, 24, 3, 11, '2026-03-24', '09:00:00', 'COMPLETED'), -(100, 2, NULL, 25, 1, 4, '2026-03-26', '10:30:00', 'MISSED'), -(101, 3, NULL, 26, 2, 9, '2026-03-28', '13:00:00', 'COMPLETED'), -(102, 4, NULL, 27, 3, 14, '2026-03-30', '14:30:00', 'COMPLETED'), -(103, 5, NULL, 28, 1, 3, '2026-04-01', '09:00:00', 'COMPLETED'), -(104, 6, NULL, 29, 2, 8, '2026-04-03', '10:30:00', 'COMPLETED'), -(105, 7, NULL, 30, 3, 13, '2026-04-05', '13:00:00', 'MISSED'), -(106, 8, NULL, 31, 1, 6, '2026-04-07', '14:30:00', 'COMPLETED'), -(107, 1, NULL, 32, 2, 7, '2026-04-09', '09:00:00', 'COMPLETED'), -(108, 2, NULL, 33, 3, 12, '2026-04-11', '10:30:00', 'CANCELLED'), -(109, 3, NULL, 34, 1, 5, '2026-04-13', '13:00:00', 'COMPLETED'), -(110, 4, NULL, 35, 2, 10, '2026-04-15', '10:00:00', 'SCHEDULED'), -(111, 5, NULL, 36, 3, 11, '2026-04-16', '14:00:00', 'SCHEDULED'); +(91, 7, 37, 16, 1, 3, '2026-03-08', '09:00:00', 'COMPLETED'), +(92, 8, 38, 17, 2, 8, '2026-03-10', '10:30:00', 'COMPLETED'), +(93, 8, 39, 18, 3, 13, '2026-03-12', '13:00:00', 'MISSED'), +(94, 4, 40, 19, 1, 6, '2026-03-14', '14:30:00', 'COMPLETED'), +(95, 5, 41, 20, 2, 7, '2026-03-16', '09:00:00', 'COMPLETED'), +(96, 8, 42, 21, 3, 12, '2026-03-18', '10:30:00', 'COMPLETED'), +(97, 2, 43, 22, 1, 5, '2026-03-20', '13:00:00', 'CANCELLED'), +(98, 8, 44, 23, 2, 10, '2026-03-22', '14:30:00', 'COMPLETED'), +(99, 8, 45, 24, 3, 11, '2026-03-24', '09:00:00', 'COMPLETED'), +(100, 8, 46, 25, 1, 4, '2026-03-26', '10:30:00', 'MISSED'), +(101, 6, 47, 26, 2, 9, '2026-03-28', '13:00:00', 'COMPLETED'), +(102, 4, 48, 27, 3, 14, '2026-03-30', '14:30:00', 'COMPLETED'), +(103, 7, 49, 28, 1, 3, '2026-04-01', '09:00:00', 'COMPLETED'), +(104, 6, 50, 29, 2, 8, '2026-04-03', '10:30:00', 'COMPLETED'), +(105, 1, 51, 30, 3, 13, '2026-04-05', '13:00:00', 'MISSED'), +(106, 4, 52, 31, 1, 6, '2026-04-07', '14:30:00', 'COMPLETED'), +(107, 1, 53, 32, 2, 7, '2026-04-09', '09:00:00', 'COMPLETED'), +(108, 2, 54, 33, 3, 12, '2026-04-11', '10:30:00', 'CANCELLED'), +(109, 3, 55, 34, 1, 5, '2026-04-13', '13:00:00', 'COMPLETED'), +(110, 4, 56, 35, 2, 10, '2026-04-15', '10:00:00', 'SCHEDULED'), +(111, 5, 57, 36, 3, 11, '2026-04-16', '14:00:00', 'SCHEDULED'); INSERT IGNORE INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES (111, '2026-03-08 09:15:00', 87.50, 'Cash', 3, 1, 3, 0, NULL, 'IN_STORE', NULL, NULL, 87.50, 0.00, 0.00, 8), @@ -2435,7 +2435,21 @@ INSERT INTO activityLog (userId, storeId, usernameSnapshot, fullNameSnapshot, ro (4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Updated appointment #60 | PUT /api/v1/appointments/60 → 200', '2026-04-14 14:20:00'), (25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-15 11:00:00'), (25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-04-15 11:10:22'), -(25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-04-15 11:30:00'); +(25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-04-15 11:30:00'), +(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-16 09:00:00'), +(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-04-16 09:45:22'), +(26, NULL,'alex.baker', 'Alex Baker', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-16 14:00:00'), +(26, NULL,'alex.baker', 'Alex Baker', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-04-16 14:10:15'), +(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-17 09:00:00'), +(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Updated appointment #110 | PUT /api/v1/appointments/110 → 200', '2026-04-17 09:30:44'), +(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-04-17 10:45:00'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-18 08:00:00'), +(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated product #12 | PUT /api/v1/products/12 → 200', '2026-04-18 08:25:30'), +(27, NULL,'alex.hall', 'Alex Hall', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-18 16:00:00'), +(27, NULL,'alex.hall', 'Alex Hall', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-04-18 16:08:33'), +(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-19 09:00:00'), +(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-04-19 10:15:00'), +(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Created a new appointment | POST /api/v1/appointments → 201', '2026-04-19 11:00:22'); INSERT IGNORE INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES (134, '2026-04-10 09:15:00', 57.67, 'Cash', 4, 1, 16, 0, NULL, 'IN_STORE', NULL, NULL, 57.67, 0.00, 0.00, 5), @@ -2453,7 +2467,15 @@ INSERT IGNORE INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeI (146, '2026-04-15 10:45:00', 80.62, 'Cash', 5, 1, 28, 0, NULL, 'IN_STORE', NULL, NULL, 80.62, 0.00, 0.00, 8), (147, '2026-04-15 13:00:00', 141.28, 'Card', 8, 2, 29, 0, NULL, 'ONLINE', NULL, NULL, 141.28, 0.00, 0.00, 14), (148, '2026-04-16 09:30:00', 97.85, 'Cash', 12, 3, 30, 0, NULL, 'IN_STORE', NULL, NULL, 97.85, 0.00, 0.00, 9), -(149, '2026-04-16 11:45:00', 89.36, 'Card', 6, 1, 31, 0, NULL, 'ONLINE', NULL, NULL, 89.36, 0.00, 0.00, 8); +(149, '2026-04-16 11:45:00', 89.36, 'Card', 6, 1, 31, 0, NULL, 'ONLINE', NULL, NULL, 89.36, 0.00, 0.00, 8), +(150, '2026-04-17 09:15:00', 112.38, 'Cash', 13, 3, 32, 0, NULL, 'IN_STORE', NULL, NULL, 112.38, 0.00, 0.00, 11), +(151, '2026-04-17 11:30:00', 67.49, 'Card', 5, 1, 33, 0, NULL, 'ONLINE', NULL, NULL, 67.49, 0.00, 0.00, 6), +(152, '2026-04-17 14:45:00', 158.20, 'Cash', 9, 2, 34, 0, NULL, 'IN_STORE', NULL, NULL, 158.20, 0.00, 0.00, 15), +(153, '2026-04-18 09:30:00', 84.76, 'Card', 14, 3, 35, 0, NULL, 'ONLINE', NULL, NULL, 84.76, 0.00, 0.00, 8), +(154, '2026-04-18 12:00:00', 203.15, 'Cash', 4, 1, 36, 0, NULL, 'IN_STORE', NULL, NULL, 203.15, 0.00, 0.00, 20), +(155, '2026-04-18 15:15:00', 45.93, 'Card', 7, 2, 37, 0, NULL, 'ONLINE', NULL, NULL, 45.93, 0.00, 0.00, 4), +(156, '2026-04-19 10:00:00', 129.84, 'Cash', 11, 3, 38, 0, NULL, 'IN_STORE', NULL, NULL, 129.84, 0.00, 0.00, 12), +(157, '2026-04-19 13:30:00', 76.50, 'Card', 6, 1, 39, 0, NULL, 'ONLINE', NULL, NULL, 76.50, 0.00, 0.00, 7); INSERT IGNORE INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VALUES (264, 134, 1, 2, 25.09), @@ -2487,4 +2509,20 @@ INSERT IGNORE INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VA (292, 148, 10, 1, 73.89), (293, 148, 24, 1, 23.96), (294, 149, 31, 2, 21.29), -(295, 149, 5, 1, 46.78); +(295, 149, 5, 1, 46.78), +(296, 150, 20, 2, 27.49), +(297, 150, 33, 1, 57.40), +(298, 151, 2, 1, 30.51), +(299, 151, 16, 2, 18.49), +(300, 152, 34, 2, 59.42), +(301, 152, 11, 1, 39.36), +(302, 153, 23, 2, 19.60), +(303, 153, 9, 1, 45.56), +(304, 154, 37, 1, 97.56), +(305, 154, 28, 1, 105.59), +(306, 155, 26, 1, 32.67), +(307, 155, 12, 1, 13.26), +(308, 156, 19, 3, 25.27), +(309, 156, 30, 1, 54.03), +(310, 157, 6, 1, 52.20), +(311, 157, 14, 2, 12.15); -- 2.49.1 From ca87b2578cbc48e1c40d46f87b49c33390fcb4a5 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 19 Apr 2026 18:26:16 -0600 Subject: [PATCH 09/34] fix validation bugs --- .../backend/controller/AuthController.java | 19 ++++++++-------- .../controller/UserAvatarController.java | 22 ++++++++++++------- .../backend/service/AppointmentService.java | 10 ++++----- .../backend/service/AvatarStorageService.java | 21 ++++++++++++++++-- .../petshop/backend/service/ChatService.java | 6 +++++ .../backend/service/CouponService.java | 4 ++++ .../main/resources/static/default-avatar.svg | 1 + 7 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 backend/src/main/resources/static/default-avatar.svg diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index a71c3777..eb91c6dc 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -309,17 +309,18 @@ public class AuthController { public ResponseEntity getAvatarFile() { User user = authHelper.getAuthenticatedUser(); - if (!avatarStorageService.hasAvatar(user)) { - return ResponseEntity.notFound().build(); + if (avatarStorageService.hasAvatar(user)) { + try { + Resource resource = avatarStorageService.loadAvatarResource(user); + MediaType mediaType = avatarStorageService.resolveMediaType(user); + return ResponseEntity.ok().contentType(mediaType).body(resource); + } catch (IllegalArgumentException ignored) { + } } - try { - Resource resource = avatarStorageService.loadAvatarResource(user); - MediaType mediaType = avatarStorageService.resolveMediaType(user); - return ResponseEntity.ok().contentType(mediaType).body(resource); - } catch (IllegalArgumentException ex) { - return ResponseEntity.notFound().build(); - } + return ResponseEntity.ok() + .contentType(MediaType.valueOf("image/svg+xml")) + .body(avatarStorageService.loadDefaultAvatarResource()); } @DeleteMapping("/me/avatar") diff --git a/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java b/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java index 602cfe54..ac7d146c 100644 --- a/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java +++ b/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java @@ -7,6 +7,7 @@ import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AvatarStorageService; import com.petshop.backend.util.ImageValidationUtil; import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; @@ -42,18 +43,23 @@ public class UserAvatarController { @PreAuthorize("isAuthenticated()") public ResponseEntity getUserAvatarFile(@PathVariable Long userId) { User user = userRepository.findById(userId).orElse(null); - if (user == null || !avatarStorageService.hasAvatar(user)) { + if (user == null) { return ResponseEntity.notFound().build(); } - try { - Resource resource = avatarStorageService.loadAvatarResource(user); - return ResponseEntity.ok() - .contentType(avatarStorageService.resolveMediaType(user)) - .body(resource); - } catch (IllegalArgumentException ex) { - return ResponseEntity.notFound().build(); + if (avatarStorageService.hasAvatar(user)) { + try { + Resource resource = avatarStorageService.loadAvatarResource(user); + return ResponseEntity.ok() + .contentType(avatarStorageService.resolveMediaType(user)) + .body(resource); + } catch (IllegalArgumentException ignored) { + } } + + return ResponseEntity.ok() + .contentType(MediaType.valueOf("image/svg+xml")) + .body(avatarStorageService.loadDefaultAvatarResource()); } @PostMapping("/{userId}/avatar") diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 5ffcc16d..832dc12e 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -143,7 +143,7 @@ public class AppointmentService { appointment.setEmployee(employee); appointment.setAppointmentDate(request.getAppointmentDate()); appointment.setAppointmentTime(request.getAppointmentTime()); - appointment.setAppointmentStatus(request.getAppointmentStatus()); + appointment.setAppointmentStatus("Scheduled"); appointment.setPet(pet); appointment = appointmentRepository.save(appointment); @@ -304,11 +304,9 @@ public class AppointmentService { } private void validateAppointmentRequest(AppointmentRequest request) { - if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) { - LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime()); - if (appointmentDateTime.isBefore(LocalDateTime.now())) { - throw new BusinessException("Booked appointments must be scheduled in the future"); - } + LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime()); + if (appointmentDateTime.isBefore(LocalDateTime.now())) { + throw new BusinessException("Appointments must be scheduled in the future"); } } diff --git a/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java b/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java index 59442d4f..5ff61021 100644 --- a/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java +++ b/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java @@ -13,6 +13,7 @@ import org.springframework.web.multipart.MultipartFile; import jakarta.annotation.PostConstruct; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -54,7 +55,11 @@ public class AvatarStorageService { public Resource loadAvatarResource(User user) { String filename = extractFilename(user.getAvatarUrl()); if (blobService.isEnabled()) { - return new ByteArrayResource(blobService.download(BLOB_CONTAINER, filename)); + try { + return new ByteArrayResource(blobService.download(BLOB_CONTAINER, filename)); + } catch (Exception ex) { + throw new IllegalArgumentException("Avatar file was not found"); + } } Path filePath = resolveStoredAvatarPath(user.getAvatarUrl()); if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) { @@ -63,6 +68,18 @@ public class AvatarStorageService { return new PathResource(filePath); } + public Resource loadDefaultAvatarResource() { + InputStream is = getClass().getResourceAsStream("/static/default-avatar.svg"); + if (is == null) { + throw new IllegalStateException("Default avatar resource not found"); + } + try { + return new ByteArrayResource(is.readAllBytes()); + } catch (IOException e) { + throw new IllegalStateException("Failed to read default avatar", e); + } + } + public void deleteAvatar(User user) throws IOException { if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) return; if (blobService.isEnabled()) { @@ -80,7 +97,7 @@ public class AvatarStorageService { } public String toOwnerAvatarUrl(User user) { - return hasAvatar(user) ? "/api/v1/users/" + user.getId() + "/avatar/file" : null; + return "/api/v1/users/" + user.getId() + "/avatar/file"; } public String toStoredAvatarUrl(String avatarFilenamePath) { diff --git a/backend/src/main/java/com/petshop/backend/service/ChatService.java b/backend/src/main/java/com/petshop/backend/service/ChatService.java index a1442d41..bf519f23 100644 --- a/backend/src/main/java/com/petshop/backend/service/ChatService.java +++ b/backend/src/main/java/com/petshop/backend/service/ChatService.java @@ -146,6 +146,12 @@ public class ChatService { } } + boolean hasContent = request.getContent() != null && !request.getContent().isBlank(); + boolean hasAttachment = request.getAttachmentUrl() != null && !request.getAttachmentUrl().isBlank(); + if (!hasContent && !hasAttachment) { + throw new BusinessException("Message must have content or an attachment"); + } + ContentFilter.validate(request.getContent()); Message message = new Message(); diff --git a/backend/src/main/java/com/petshop/backend/service/CouponService.java b/backend/src/main/java/com/petshop/backend/service/CouponService.java index 9625cfed..18ff269c 100644 --- a/backend/src/main/java/com/petshop/backend/service/CouponService.java +++ b/backend/src/main/java/com/petshop/backend/service/CouponService.java @@ -89,6 +89,10 @@ public class CouponService { } private void updateCouponFields(Coupon coupon, CouponRequest request) { + if ("PERCENTAGE".equalsIgnoreCase(request.getDiscountType()) + && request.getDiscountValue().compareTo(new BigDecimal("100")) >= 0) { + throw new BusinessException("Percentage discount must be less than 100"); + } coupon.setCouponCode(request.getCouponCode()); coupon.setDiscountType(request.getDiscountType()); coupon.setDiscountValue(request.getDiscountValue()); diff --git a/backend/src/main/resources/static/default-avatar.svg b/backend/src/main/resources/static/default-avatar.svg new file mode 100644 index 00000000..abacf491 --- /dev/null +++ b/backend/src/main/resources/static/default-avatar.svg @@ -0,0 +1 @@ + -- 2.49.1 From 4aa0817000c322510083a143d666e78d0f44b293 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 19 Apr 2026 18:17:42 -0600 Subject: [PATCH 10/34] fix profile placeholder flash --- android/app/src/main/res/layout/fragment_profile.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/res/layout/fragment_profile.xml b/android/app/src/main/res/layout/fragment_profile.xml index 34a6e33c..1de5db19 100644 --- a/android/app/src/main/res/layout/fragment_profile.xml +++ b/android/app/src/main/res/layout/fragment_profile.xml @@ -45,7 +45,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" - android:text="First Last" + android:text="" android:textColor="@color/white" android:textSize="22sp" android:textStyle="bold" /> @@ -87,7 +87,7 @@ android:id="@+id/tvProfileEmail" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="No email loaded" + android:text="" android:textColor="@color/text_dark" android:textSize="16sp" /> @@ -133,7 +133,7 @@ android:id="@+id/tvProfilePhone" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="No phone loaded" + android:text="" android:textColor="@color/text_dark" android:textSize="16sp" /> @@ -178,7 +178,7 @@ android:id="@+id/tvProfileRole" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="No role loaded" + android:text="" android:textSize="16sp" android:textColor="@color/accent_coral"/> @@ -208,7 +208,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:visibility="gone" + android:visibility="visible" android:indeterminateTint="@color/accent_coral"/> \ No newline at end of file -- 2.49.1 From 7072a7a2751879cf5ac9bac09e09fbc8bf83b2a4 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 19 Apr 2026 18:56:58 -0600 Subject: [PATCH 11/34] add project README --- README.md | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6575e1cd..959d9825 100644 --- a/README.md +++ b/README.md @@ -1 +1,113 @@ -# group-2-threaded-project-petshop \ No newline at end of file +# PetShop + +A pet store management application with a Spring Boot API serving three clients: a Next.js web app, a JavaFX desktop app, and an Android app. + +Handles product sales, pet adoption, appointment booking, real-time chat, AI assistance, payments (Stripe), email notifications (Resend), and file storage (Azure Blob). + +## Tech Stack + +| Layer | Technology | +|-------|------------| +| API | Java 25, Spring Boot 4, Spring Security (JWT), Hibernate | +| Database | MySQL 8.0, Flyway migrations | +| Web | Next.js 16, React 19, Tailwind CSS 4 | +| Desktop | JavaFX, Maven | +| Android | Kotlin, Hilt, Retrofit, CameraX | +| Infra | Docker, Azure Container Apps | + +## Project Structure + +``` +main/ + backend/ Spring Boot REST API + web/ Next.js frontend + desktop/ JavaFX desktop client + android/ Android mobile app +``` + +## Prerequisites + +- Java 25 +- Node.js 18+ +- Docker +- Maven +- Android Studio (for mobile) + +## Getting Started + +### 1. Start the database + +```sh +cd backend +docker compose -f docker-compose.dev.yml up -d +``` + +### 2. Configure the backend + +```sh +cd backend +cp .env.example .env +``` + +Fill in `.env` with your keys: + +``` +JWT_SECRET= +STRIPE_SECRET_KEY=sk_test_... +OPENROUTER_API_KEY=sk-or-v1-... +RESEND_API_KEY=re_... +RESEND_FROM=PetShop +``` + +### 3. Run the backend + +```sh +cd backend +mvn spring-boot:run +``` + +The API starts at `http://localhost:8080`. Flyway runs migrations and seeds data automatically on first boot. + +### 4. Run the web frontend + +```sh +cd web +cp .env.example .env.local +npm install +npm run dev +``` + +The web app starts at `http://localhost:3000`. + +### 5. Run the desktop client (optional) + +```sh +cd desktop +cp connectionpetstore.properties.example connectionpetstore.properties +mvn javafx:run +``` + +### 6. Run the Android app (optional) + +Open `android/` in Android Studio and run on an emulator or device. + +## API + +A Postman collection is available at `backend/postman/`. Key endpoint groups: + +- `/api/auth` -- registration, login, password reset +- `/api/products` -- catalog and inventory +- `/api/pets` -- listings and adoption +- `/api/appointments` -- booking +- `/api/cart`, `/api/sales`, `/api/refunds` -- transactions +- `/api/chat` -- messaging and AI assistant +- `/ws` -- WebSocket (STOMP) for real-time updates + +## Docker (full stack) + +```sh +cd backend +docker compose up --build -d +``` + +Starts the API and MySQL together. The web frontend has its own Dockerfile for independent deployment. -- 2.49.1 From c14413a634e3afa5b45bb886f824980db94a167e Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 19 Apr 2026 19:04:13 -0600 Subject: [PATCH 12/34] add client run instructions --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/README.md b/README.md index 959d9825..84c0f597 100644 --- a/README.md +++ b/README.md @@ -111,3 +111,83 @@ docker compose up --build -d ``` Starts the API and MySQL together. The web frontend has its own Dockerfile for independent deployment. + +## Running the Web App + +Requires Node.js 18+. + +```sh +cd web +cp .env.example .env.local +npm install +npm run dev +``` + +Open `http://localhost:3000`. The app proxies API calls to the backend at `http://localhost:8080` by default. + +To point at a different backend, edit `BACKEND_URL` and `NEXT_PUBLIC_BACKEND_URL` in `.env.local`. + +For a production build: + +```sh +npm run build +npm run start +``` + +## Running the Desktop App (JavaFX) + +Requires IntelliJ IDEA and Java 25+. + +1. Open the `desktop/` directory in IntelliJ. +2. Copy `connectionpetstore.properties.example` to `connectionpetstore.properties` and edit it to match your database. The defaults expect the dev Docker database: + +``` +url=jdbc:mysql://127.0.0.1:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC +user=petshop +password=petshop +``` + +3. Open **View > Tool Windows > Maven** and click **Reload All Maven Projects**. +4. Expand **Plugins > javafx** and double-click **javafx:run**. + +Default accounts seeded on first run: + +| Role | Username | Password | +|------|----------|----------| +| Admin | `admin` | `admin123` | +| Staff | `staff` | `staff123` | + +## Running the Android App + +Requires Android Studio and the Android SDK (min API 24). + +1. Copy `local.properties.template` to `local.properties` and set `sdk.dir` to your Android SDK path. +2. Configure the backend URLs in `local.properties`: + +```properties +# Emulator — 10.0.2.2 maps to the host machine's localhost +petstore.backend.emulatorUrl=http\://10.0.2.2\:8080/ + +# Physical device — use the host machine's LAN IP +petstore.backend.deviceUrl=http\://192.168.x.x\:8080/ +``` + +3. Open the `android/` directory in Android Studio. +4. Sync Gradle, then run on an emulator or connected device. + +## Running the Backend + +Requires IntelliJ IDEA and Java 25+. + +1. Open the `backend/` directory in IntelliJ. +2. Copy `.env.example` to `.env` and fill in your API keys. +3. Start the database using Docker from IntelliJ's **Services** panel, or from a terminal: + +```sh +cd backend +docker compose -f docker-compose.dev.yml up -d +``` + +4. Run the `BackendApplication` main class from IntelliJ. + +The API starts at `http://localhost:8080`. Flyway runs migrations and seeds data automatically on first boot. -- 2.49.1 From c2474f941e4886b1a1dd13b2549da542cb4f8b83 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 19 Apr 2026 19:05:07 -0600 Subject: [PATCH 13/34] configurable rate limiter --- .../java/com/petshop/backend/security/RateLimitFilter.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/petshop/backend/security/RateLimitFilter.java b/backend/src/main/java/com/petshop/backend/security/RateLimitFilter.java index 567d4219..31f2434f 100644 --- a/backend/src/main/java/com/petshop/backend/security/RateLimitFilter.java +++ b/backend/src/main/java/com/petshop/backend/security/RateLimitFilter.java @@ -10,6 +10,8 @@ import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.beans.factory.annotation.Value; + import java.io.IOException; import java.time.Duration; import java.util.Map; @@ -24,6 +26,9 @@ public class RateLimitFilter extends OncePerRequestFilter { "/api/v1/auth/reset-password", new int[]{10, 15} ); + @Value("${app.rate-limit-enabled:true}") + private boolean enabled; + private final RateLimiterService rateLimiterService; private final ApiErrorResponder apiErrorResponder; @@ -37,7 +42,7 @@ public class RateLimitFilter extends OncePerRequestFilter { @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String path = request.getRequestURI(); - int[] rule = RULES.get(path); + int[] rule = enabled ? RULES.get(path) : null; if (rule != null) { String ip = extractIp(request); -- 2.49.1 From 52892a731c6a898c040889a87b47fc3f01031953 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:18:28 -0600 Subject: [PATCH 14/34] added comments to android --- .../activities/ForgotPasswordActivity.java | 7 ++ .../activities/HomeActivity.java | 4 +- .../activities/MainActivity.java | 5 +- .../adapters/ActivityLogAdapter.java | 27 +++++ .../adapters/AdoptionAdapter.java | 18 +++ .../adapters/AppointmentAdapter.java | 24 ++++ .../adapters/BlackTextArrayAdapter.java | 1 + .../petstoremobile/adapters/ChatAdapter.java | 18 +++ .../adapters/CouponAdapter.java | 24 ++++ .../adapters/CustomerAdapter.java | 25 +++++ .../adapters/EmployeeAdapter.java | 24 ++++ .../adapters/InventoryAdapter.java | 25 ++++- .../adapters/MessageAdapter.java | 66 ++++++++++- .../petstoremobile/adapters/PetAdapter.java | 36 +++++- .../adapters/ProductAdapter.java | 24 ++++ .../adapters/ProductSupplierAdapter.java | 21 ++++ .../adapters/PurchaseOrderAdapter.java | 15 +++ .../petstoremobile/adapters/SaleAdapter.java | 15 +++ .../adapters/ServiceAdapter.java | 22 +++- .../adapters/SupplierAdapter.java | 25 ++++- .../adapters/WhiteTextArrayAdapter.java | 5 +- .../petstoremobile/api/ActivityLogApi.java | 2 + .../petstoremobile/api/AdoptionApi.java | 7 ++ .../petstoremobile/api/AppointmentApi.java | 9 +- .../petstoremobile/api/CategoryApi.java | 2 + .../example/petstoremobile/api/ChatApi.java | 5 +- .../petstoremobile/api/CustomerApi.java | 7 ++ .../petstoremobile/api/EmployeeApi.java | 12 +- .../petstoremobile/api/InventoryApi.java | 12 +- .../petstoremobile/api/MessageApi.java | 6 +- .../example/petstoremobile/api/PetApi.java | 8 +- .../petstoremobile/api/ProductApi.java | 13 ++- .../api/ProductSupplierApi.java | 10 +- .../petstoremobile/api/PurchaseOrderApi.java | 3 + .../example/petstoremobile/api/RefundApi.java | 6 + .../example/petstoremobile/api/SaleApi.java | 4 + .../petstoremobile/api/ServiceApi.java | 2 +- .../example/petstoremobile/api/StoreApi.java | 4 + .../example/petstoremobile/api/UserApi.java | 3 + .../petstoremobile/api/auth/TokenManager.java | 1 + .../petstoremobile/dtos/ActivityLogDTO.java | 3 + .../petstoremobile/dtos/AdoptionDTO.java | 3 + .../petstoremobile/dtos/AppointmentDTO.java | 3 + .../example/petstoremobile/dtos/AuthDTO.java | 4 +- .../dtos/AvatarUploadResponse.java | 3 + .../dtos/BulkDeleteRequest.java | 3 + .../petstoremobile/dtos/CategoryDTO.java | 3 + .../petstoremobile/dtos/ConversationDTO.java | 3 + .../petstoremobile/dtos/CouponDTO.java | 3 + .../petstoremobile/dtos/CustomerDTO.java | 3 + .../petstoremobile/dtos/DropdownDTO.java | 3 + .../petstoremobile/dtos/EmployeeDTO.java | 3 + .../petstoremobile/dtos/ErrorResponse.java | 5 +- .../petstoremobile/dtos/InventoryDTO.java | 3 + .../petstoremobile/dtos/MessageDTO.java | 3 + .../petstoremobile/dtos/PageResponse.java | 4 +- .../example/petstoremobile/dtos/PetDTO.java | 3 + .../petstoremobile/dtos/ProductDTO.java | 3 + .../dtos/ProductSupplierDTO.java | 3 + .../petstoremobile/dtos/PurchaseOrderDTO.java | 3 + .../petstoremobile/dtos/RefundDTO.java | 3 + .../example/petstoremobile/dtos/SaleDTO.java | 3 + .../dtos/SendMessageRequest.java | 3 + .../petstoremobile/dtos/ServiceDTO.java | 3 + .../example/petstoremobile/dtos/StoreDTO.java | 3 + .../petstoremobile/dtos/SupplierDTO.java | 3 + .../dtos/UpdateConversationStatusRequest.java | 3 + .../example/petstoremobile/dtos/UserDTO.java | 3 + .../fragments/ChatFragment.java | 81 +++++++++++++ .../fragments/ListFragment.java | 7 +- .../fragments/ProfileFragment.java | 6 + .../listfragments/ActivityLogFragment.java | 27 +++++ .../listfragments/AdoptionFragment.java | 63 +++++++++++ .../listfragments/AnalyticsFragment.java | 61 +++++++++- .../listfragments/AppointmentFragment.java | 69 ++++++++++++ .../listfragments/CouponFragment.java | 39 +++++++ .../listfragments/CustomerFragment.java | 33 ++++++ .../listfragments/InventoryFragment.java | 51 +++++++++ .../fragments/listfragments/PetFragment.java | 60 ++++++++++ .../listfragments/ProductFragment.java | 42 +++++++ .../ProductSupplierFragment.java | 51 +++++++++ .../listfragments/PurchaseOrderFragment.java | 45 ++++++++ .../fragments/listfragments/SaleFragment.java | 54 +++++++++ .../listfragments/ServiceFragment.java | 39 +++++++ .../listfragments/StaffFragment.java | 36 ++++++ .../listfragments/SupplierFragment.java | 42 +++++++ .../AdoptionDetailFragment.java | 49 ++++++++ .../AppointmentDetailFragment.java | 64 +++++++++++ .../detailfragments/CouponDetailFragment.java | 21 ++++ .../CustomerDetailFragment.java | 30 +++++ .../InventoryDetailFragment.java | 48 ++++++++ .../ProductDetailFragment.java | 45 ++++++++ .../ProductSupplierDetailFragment.java | 39 +++++++ .../PurchaseOrderDetailFragment.java | 15 +++ .../detailfragments/RefundFragment.java | 53 ++++++++- .../detailfragments/SaleDetailFragment.java | 60 ++++++++++ .../ServiceDetailFragment.java | 39 +++++++ .../detailfragments/StaffDetailFragment.java | 39 +++++++ .../SupplierDetailFragment.java | 39 +++++++ .../PetProfileFragment.java | 27 +++++ .../example/petstoremobile/models/Chat.java | 3 + .../petstoremobile/models/Message.java | 3 + .../example/petstoremobile/models/Sale.java | 3 + .../repositories/ActivityLogRepository.java | 6 + .../repositories/AdoptionRepository.java | 3 + .../repositories/AppointmentRepository.java | 3 + .../repositories/AuthRepository.java | 3 + .../repositories/BaseRepository.java | 2 +- .../repositories/CategoryRepository.java | 3 + .../repositories/ChatRepository.java | 2 +- .../repositories/CouponRepository.java | 3 + .../repositories/CustomerRepository.java | 3 + .../repositories/EmployeeRepository.java | 3 + .../repositories/InventoryRepository.java | 3 + .../repositories/PetRepository.java | 3 + .../repositories/ProductRepository.java | 3 + .../ProductSupplierRepository.java | 3 + .../repositories/PurchaseOrderRepository.java | 3 + .../repositories/SaleRepository.java | 3 + .../repositories/ServiceRepository.java | 3 + .../repositories/StoreRepository.java | 3 + .../repositories/SupplierRepository.java | 3 + .../repositories/UserRepository.java | 3 + .../services/ChatNotificationService.java | 4 +- .../petstoremobile/utils/EventDecorator.java | 6 + .../petstoremobile/utils/FileUtils.java | 10 ++ .../petstoremobile/utils/SelectionHelper.java | 34 +++++- .../petstoremobile/utils/SpinnerUtils.java | 7 +- .../viewmodels/ActivityLogListViewModel.java | 39 +++++++ .../viewmodels/AdoptionDetailViewModel.java | 74 +++++++++++- .../viewmodels/AdoptionListViewModel.java | 24 ++++ .../viewmodels/AnalyticsViewModel.java | 73 ++++++++++++ .../AppointmentDetailViewModel.java | 4 +- .../viewmodels/AppointmentListViewModel.java | 24 ++++ .../viewmodels/AuthViewModel.java | 15 ++- .../viewmodels/ChatListViewModel.java | 54 +++++++++ .../viewmodels/CouponDetailViewModel.java | 18 +++ .../viewmodels/CouponListViewModel.java | 20 ++++ .../viewmodels/CustomerDetailViewModel.java | 9 ++ .../viewmodels/CustomerListViewModel.java | 26 +++++ .../viewmodels/InventoryDetailViewModel.java | 39 +++++++ .../viewmodels/InventoryListViewModel.java | 24 ++++ .../viewmodels/PetListViewModel.java | 37 ++++++ .../viewmodels/PetProfileViewModel.java | 9 ++ .../viewmodels/ProductDetailViewModel.java | 33 ++++++ .../viewmodels/ProductListViewModel.java | 21 ++++ .../ProductSupplierDetailViewModel.java | 43 +++++++ .../ProductSupplierListViewModel.java | 28 +++++ .../PurchaseOrderDetailViewModel.java | 3 + .../PurchaseOrderListViewModel.java | 21 ++++ .../viewmodels/RefundViewModel.java | 39 +++++++ .../viewmodels/SaleDetailViewModel.java | 106 ++++++++++++++++++ .../viewmodels/SaleListViewModel.java | 34 ++++++ .../viewmodels/ServiceDetailViewModel.java | 24 ++++ .../viewmodels/ServiceListViewModel.java | 17 +++ .../viewmodels/StaffDetailViewModel.java | 27 +++++ .../viewmodels/StaffListViewModel.java | 33 ++++++ .../viewmodels/SupplierDetailViewModel.java | 24 ++++ .../viewmodels/SupplierListViewModel.java | 17 +++ .../websocket/StompChatManager.java | 80 ++++++++++--- 160 files changed, 3098 insertions(+), 81 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java index 82ea9934..ef08f5ee 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/ForgotPasswordActivity.java @@ -32,6 +32,9 @@ public class ForgotPasswordActivity extends AppCompatActivity { private ActivityForgotPasswordBinding binding; + /** + * Set the content view for forget password page + */ @Override protected void onCreate(Bundle savedInstanceState) { EdgeToEdge.enable(this); @@ -54,6 +57,10 @@ public class ForgotPasswordActivity extends AppCompatActivity { binding.btnBackToLogin.setOnClickListener(v -> finish()); } + /** + * A function to send a reset link to the given email address. + * Calls the forgotPassword endpoint. To send the reset link + * */ private void sendResetLink(String email) { binding.btnSubmit.setEnabled(false); diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java index 01b4ca24..9e5fd6d0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java @@ -78,7 +78,7 @@ public class HomeActivity extends AppCompatActivity { @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); - setIntent(intent); // Set the new intent so fragments can access updated extras + setIntent(intent); handleIntent(intent); } @@ -103,7 +103,7 @@ public class HomeActivity extends AppCompatActivity { } /** - * Requests POST_NOTIFICATIONS permission from the user if running on Android 13 and above. + * Requests for notification permission from the user if running on Android 13 and above. */ private void requestNotificationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java index 11925052..b7aa1f1b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java @@ -98,7 +98,7 @@ public class MainActivity extends AppCompatActivity { } /** - * Executes the login process using the AuthViewModel and handles the authentication response. + * Perform login process using the AuthViewModel and handles the authentication response. */ private void performLogin(String username, String password) { viewModel.login(username, password).observe(this, resource -> { @@ -112,6 +112,7 @@ public class MainActivity extends AppCompatActivity { case SUCCESS: if (resource.data != null) { String role = resource.data.getRole(); + //Check if role is staff/admin or customer if ("CUSTOMER".equalsIgnoreCase(role)) { UIUtils.setViewsEnabled(true, binding.btnLogin); binding.tvLoginStatus.setText("Customers are not allowed to log in"); @@ -132,7 +133,7 @@ public class MainActivity extends AppCompatActivity { } /** - * Retrieves the logged-in user's profile information to save their ID before navigating to the home screen. + * Retrieves the user's profile information to save their ID before navigating to the home screen. */ private void fetchUserIdAndNavigate() { viewModel.getMe().observe(this, resource -> { diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ActivityLogAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ActivityLogAdapter.java index eb571ca2..ecd34994 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ActivityLogAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ActivityLogAdapter.java @@ -25,10 +25,16 @@ public class ActivityLogAdapter extends RecyclerView.Adapter items; + /** + * Constructor for the ActivityLogAdapter. + */ public ActivityLogAdapter(List items) { this.items = items; } + /** + * Inflates the layout for an activity log item and creates a new ViewHolder. + */ @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -37,6 +43,9 @@ public class ActivityLogAdapter extends RecyclerView.Adapter adoptionList, OnAdoptionClickListener listener) { this.adoptionList = adoptionList; this.listener = listener; @@ -39,11 +42,17 @@ public class AdoptionAdapter extends RecyclerView.Adapter getSelectedKeys() { return selectionHelper.getSelectedKeys(); } + /** + * Resets the selection state, deselecting all items. + */ @Override public void clearSelection() { selectionHelper.clearSelection(); @@ -58,6 +67,9 @@ public class AdoptionAdapter extends RecyclerView.Adapter appointmentList, OnAppointmentClickListener appointmentClickListener) { this.appointmentList = appointmentList; @@ -40,25 +43,40 @@ public class AppointmentAdapter extends RecyclerView.Adapter getSelectedKeys() { return selectionHelper.getSelectedKeys(); } + /** + * Resets the selection state, deselecting all items. + */ @Override public void clearSelection() { selectionHelper.clearSelection(); } + /** + * ViewHolder class that holds references to the UI components for an appointment item. + */ public static class AppointmentViewHolder extends RecyclerView.ViewHolder { private final ItemAppointmentBinding binding; + /** + * Initializes the ViewHolder by finding the views within the item layout. + */ public AppointmentViewHolder(@NonNull ItemAppointmentBinding binding) { super(binding.getRoot()); this.binding = binding; } } + /** + * Inflates the layout for an appointment item and creates the ViewHolder. + */ @NonNull @Override public AppointmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -66,6 +84,9 @@ public class AppointmentAdapter extends RecyclerView.Adapter extends ArrayAdapter { public BlackTextArrayAdapter(@NonNull Context context, int resource, @NonNull T[] objects) { super(context, resource, objects); diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java index 972ac56d..329adfe8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java @@ -20,11 +20,17 @@ public class ChatAdapter extends RecyclerView.Adapter chatList, OnChatClickListener listener) { this.chatList = chatList; this.listener = listener; } + /** + * Inflates the layout for a chat item and creates the ViewHolder. + */ @NonNull @Override public ChatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -32,6 +38,9 @@ public class ChatAdapter extends RecyclerView.Adapter listener.onChatClick(chat)); } + /** + * Returns the total number of chat items in the list. + */ @Override public int getItemCount() { return chatList.size(); } + /** + * ViewHolder class that holds references to the UI components for a chat item. + */ public static class ChatViewHolder extends RecyclerView.ViewHolder { final ItemChatBinding binding; + /** + * Initializes the ViewHolder with the chat item's view binding. + */ public ChatViewHolder(@NonNull ItemChatBinding binding) { super(binding.getRoot()); this.binding = binding; diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/CouponAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/CouponAdapter.java index ff9bbe65..10994ddd 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/CouponAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/CouponAdapter.java @@ -30,11 +30,17 @@ public class CouponAdapter extends RecyclerView.Adapter coupons, OnCouponClickListener listener) { this.coupons = coupons; this.listener = listener; } + /** + * Inflates the layout for a coupon item and creates the ViewHolder. + */ @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -42,6 +48,9 @@ public class CouponAdapter extends RecyclerView.Adapter toggleSelection(coupon.getCouponId())); } + /** + * Toggles the selection state of a specific coupon by its ID. + */ private void toggleSelection(Long id) { if (selectedIds.contains(id)) { selectedIds.remove(id); @@ -105,6 +117,9 @@ public class CouponAdapter extends RecyclerView.Adapter getSelectedIds() { return selectedIds; } + /** + * Returns the total number of coupons in the list. + */ @Override public int getItemCount() { return coupons.size(); @@ -125,6 +146,9 @@ public class CouponAdapter extends RecyclerView.Adapter list, OnCustomerClickListener listener) { this.list = list; this.listener = listener; } + /** + * Sets the base URL for fetching customer profile images. + */ public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + + /** + * Sets the authentication token for fetching images. + */ public void setToken(String token) { this.token = token; } + /** + * ViewHolder class for customer items. + */ public static class CustomerViewHolder extends RecyclerView.ViewHolder { final ItemCustomerBinding binding; + /** + * Initializes the ViewHolder with view binding. + */ public CustomerViewHolder(@NonNull ItemCustomerBinding binding) { super(binding.getRoot()); this.binding = binding; } } + /** + * Inflates the layout for a customer item and creates the ViewHolder. + */ @NonNull @Override public CustomerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -49,6 +68,9 @@ public class CustomerAdapter extends RecyclerView.Adapter listener.onCustomerClick(position)); } + /** + * Returns the total number of customers in the list. + */ @Override public int getItemCount() { return list.size(); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java index 8213dbd0..af8f99e5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/EmployeeAdapter.java @@ -25,28 +25,46 @@ public class EmployeeAdapter extends RecyclerView.Adapter list, OnEmployeeClickListener listener) { this.list = list; this.listener = listener; } + /** + * Sets the base URL for fetching employee profile images. + */ public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + /** + * Sets the authentication token for fetching images. + */ public void setToken(String token) { this.token = token; } + /** + * ViewHolder class for employee items. + */ public static class EmployeeViewHolder extends RecyclerView.ViewHolder { private final ItemEmployeeBinding binding; + /** + * Initializes the ViewHolder with view binding. + */ public EmployeeViewHolder(@NonNull ItemEmployeeBinding binding) { super(binding.getRoot()); this.binding = binding; } } + /** + * Inflates the layout for an employee item and creates the ViewHolder. + */ @NonNull @Override public EmployeeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -55,6 +73,9 @@ public class EmployeeAdapter extends RecyclerView.Adapter listener.onEmployeeClick(position)); } + /** + * Returns the total number of employees in the list. + */ @Override public int getItemCount() { return list.size(); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java index 9fbc1481..a015bb97 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java @@ -27,6 +27,9 @@ public class InventoryAdapter extends RecyclerView.Adapter inventoryList, OnInventoryClickListener clickListener) { this.inventoryList = inventoryList; this.clickListener = clickListener; @@ -43,25 +46,40 @@ public class InventoryAdapter extends RecyclerView.Adapter getSelectedKeys() { return selectionHelper.getSelectedKeys(); } + /** + * Resets the selection state, deselecting all items. + */ @Override public void clearSelection() { selectionHelper.clearSelection(); } + /** + * ViewHolder class that holds references to the UI components for an inventory item. + */ public static class InventoryViewHolder extends RecyclerView.ViewHolder { final ItemInventoryBinding binding; + /** + * Initializes the ViewHolder with view binding. + */ public InventoryViewHolder(@NonNull ItemInventoryBinding binding) { super(binding.getRoot()); this.binding = binding; } } + /** + * Inflates the layout for an inventory item and creates the ViewHolder. + */ @NonNull @Override public InventoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -69,6 +87,9 @@ public class InventoryAdapter extends RecyclerView.Adapter messages, Long currentUserId) { this.messages = messages; this.currentUserId = currentUserId; setHasStableIds(true); } + /** + * Returns an ID for each message. + */ @Override public long getItemId(int position) { Message m = messages.get(position); return m.getId() != null ? m.getId() : position; } + /** + * Updates the current user's ID and refreshes the list. + */ public void setCurrentUserId(Long id) { this.currentUserId = id; notifyDataSetChanged(); } + /** + * Updates the staff ID to identify staff messages in the UI. + */ public void setStaffId(Long id) { this.staffId = id; notifyDataSetChanged(); } + /** + * Sets the authentication token. + */ public void setToken(String token) { this.token = token; } + /** + * Sets the base API URL. + */ public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + /** + * Sets a listener for clicks on message attachments. + */ public void setOnAttachmentClickListener(OnAttachmentClickListener listener) { this.attachmentClickListener = listener; } + /** + * Determines if a message is 'sent' or 'received' based on the sender's ID. + */ @Override public int getItemViewType(int position) { Message m = messages.get(position); @@ -80,6 +104,9 @@ public class MessageAdapter extends RecyclerView.Adapter i void onSelectionChanged(int selectedCount); } - //Constructor + /** + * Constructor for PetAdapter. + */ public PetAdapter(List petList, OnPetClickListener petClickListener) { this.petList = petList; this.petClickListener = petClickListener; @@ -48,35 +50,54 @@ public class PetAdapter extends RecyclerView.Adapter i }); } + /** + * Sets the base URL for fetching pet images from the server. + */ public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + /** + * Sets the authentication token + */ public void setToken(String token) { this.token = token; } + /** + * Returns a list of IDs for the currently selected pet items. + */ @Override public List getSelectedKeys() { return selectionHelper.getSelectedKeys(); } + /** + * Resets the selection state, deselecting all items. + */ @Override public void clearSelection() { selectionHelper.clearSelection(); } - // Get the controls of each row in recycler view + /** + * ViewHolder class that holds references to the UI components for a pet item. + */ public static class PetViewHolder extends RecyclerView.ViewHolder { private final ItemPetBinding binding; + /** + * Initializes the ViewHolder with view binding. + */ public PetViewHolder(@NonNull ItemPetBinding binding) { super(binding.getRoot()); this.binding = binding; } } - // Create a new row view + /** + * Inflates the layout for a pet item and creates the ViewHolder. + */ @NonNull @Override public PetViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -84,7 +105,9 @@ public class PetAdapter extends RecyclerView.Adapter i return new PetViewHolder(binding); } - //populate the row with pet data + /** + * Binds pet data to the UI components. + */ @Override public void onBindViewHolder(@NonNull PetViewHolder holder, int position) { PetDTO pet = petList.get(position); @@ -103,7 +126,7 @@ public class PetAdapter extends RecyclerView.Adapter i binding.tvPetStatus.setText(pet.getPetStatus()); - //Set the status color depending on availability. If available, green, If Pending, yellow, otherwise red + //Set the status color depending on availability if (pet.getPetStatus() != null) { switch (pet.getPetStatus()) { case "Available": @@ -157,6 +180,9 @@ public class PetAdapter extends RecyclerView.Adapter i }); } + /** + * Returns the total number of pet items in the list. + */ @Override public int getItemCount() { return petList.size(); diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java index f5f897cc..9adea207 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -22,28 +22,46 @@ public class ProductAdapter extends RecyclerView.Adapter productList, OnProductClickListener listener) { this.productList = productList; this.listener = listener; } + /** + * Sets the base URL for fetching product images. + */ public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + /** + * Sets the authentication token + */ public void setToken(String token) { this.token = token; } + /** + * ViewHolder class for product items. + */ public static class ProductViewHolder extends RecyclerView.ViewHolder { final ItemProductBinding binding; + /** + * Initializes the ViewHolder with view binding. + */ public ProductViewHolder(@NonNull ItemProductBinding binding) { super(binding.getRoot()); this.binding = binding; } } + /** + * Inflates the layout for a product item and creates the ViewHolder. + */ @NonNull @Override public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -51,6 +69,9 @@ public class ProductAdapter extends RecyclerView.Adapter listener.onProductClick(position)); } + /** + * Returns the total number of product items in the list. + */ @Override public int getItemCount() { return productList.size(); } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java index 231af741..c5c07e00 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java @@ -25,6 +25,9 @@ public class ProductSupplierAdapter extends RecyclerView.Adapter list, OnProductSupplierClickListener listener) { this.list = list; this.listener = listener; @@ -41,25 +44,40 @@ public class ProductSupplierAdapter extends RecyclerView.Adapter getSelectedKeys() { return selectionHelper.getSelectedKeys(); } + /** + * Clears all selected items and exits selection mode. + */ @Override public void clearSelection() { selectionHelper.clearSelection(); } + /** + * ViewHolder for Product-Supplier relationship items. + */ public static class PSViewHolder extends RecyclerView.ViewHolder { final ItemProductSupplierBinding binding; + /** + * Initializes the ViewHolder with view binding. + */ public PSViewHolder(@NonNull ItemProductSupplierBinding binding) { super(binding.getRoot()); this.binding = binding; } } + /** + * Inflates the layout for a Product-Supplier item. + */ @NonNull @Override public PSViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -67,6 +85,9 @@ public class ProductSupplierAdapter extends RecyclerView.Adapter list, OnPurchaseOrderClickListener listener) { this.list = list; this.listener = listener; } + /** + * ViewHolder for Purchase Order items. + */ public static class POViewHolder extends RecyclerView.ViewHolder { final ItemPurchaseOrderBinding binding; + /** + * Initializes the ViewHolder with view binding. + */ public POViewHolder(@NonNull ItemPurchaseOrderBinding binding) { super(binding.getRoot()); this.binding = binding; } } + /** + * Inflates the layout for a Purchase Order item. + */ @NonNull @Override public POViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -38,6 +50,9 @@ public class PurchaseOrderAdapter extends RecyclerView.Adapter saleList, OnSaleClickListener listener) { this.saleList = saleList; this.listener = listener; } + /** + * ViewHolder for Sale record items. + */ public static class SaleViewHolder extends RecyclerView.ViewHolder { final ItemSaleBinding binding; + /** + * Initializes the ViewHolder with view binding. + */ public SaleViewHolder(@NonNull ItemSaleBinding binding) { super(binding.getRoot()); this.binding = binding; } } + /** + * Inflates the layout for a Sale record item. + */ @NonNull @Override public SaleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -41,6 +53,9 @@ public class SaleAdapter extends RecyclerView.Adapter serviceList, OnServiceClickListener clickListener) { this.serviceList = serviceList; this.clickListener = clickListener; @@ -47,29 +50,40 @@ public class ServiceAdapter extends RecyclerView.Adapter getSelectedKeys() { return selectionHelper.getSelectedKeys(); } + /** + * Clears all selected items and exits selection mode. + */ @Override public void clearSelection() { selectionHelper.clearSelection(); } /** - * ViewHolder class for service items. + * ViewHolder for Service items. */ public static class ServiceViewHolder extends RecyclerView.ViewHolder { final ItemServiceBinding binding; + /** + * Initializes the ViewHolder with view binding. + */ public ServiceViewHolder(@NonNull ItemServiceBinding binding) { super(binding.getRoot()); this.binding = binding; } } - // Create a new row view + /** + * Inflates the layout for a Service item. + */ @NonNull @Override public ServiceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -77,7 +91,9 @@ public class ServiceAdapter extends RecyclerView.Adapter supplierList, OnSupplierClickListener supplierClickListener) { this.supplierList = supplierList; this.supplierClickListener = supplierClickListener; @@ -43,27 +45,40 @@ public class SupplierAdapter extends RecyclerView.Adapter getSelectedKeys() { return selectionHelper.getSelectedKeys(); } + /** + * Clears all selected items and exits selection mode. + */ @Override public void clearSelection() { selectionHelper.clearSelection(); } - // Get the controls of each row in recycler view + /** + * ViewHolder for Supplier items. + */ public static class SupplierViewHolder extends RecyclerView.ViewHolder { final ItemSupplierBinding binding; + /** + * Initializes the ViewHolder with view binding. + */ public SupplierViewHolder(@NonNull ItemSupplierBinding binding) { super(binding.getRoot()); this.binding = binding; } } - // Create a new row view + /** + * Inflates the layout for a Supplier item. + */ @NonNull @Override public SupplierViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -71,7 +86,9 @@ public class SupplierAdapter extends RecyclerView.Adapter extends ArrayAdapter { public WhiteTextArrayAdapter(@NonNull Context context, int resource, @NonNull T[] objects) { super(context, resource, objects); diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java index 12ceb5bd..4ff8af6d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java @@ -8,8 +8,10 @@ import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; +// api calls to get activity logs public interface ActivityLogApi { + // Get activity logs with filters @GET("api/v1/activity-logs") Call> getActivityLogs( @Query("limit") int limit, diff --git a/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java b/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java index 6e1de73d..fa9114b5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/AdoptionApi.java @@ -14,8 +14,10 @@ import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; +// api calls for adoptions public interface AdoptionApi { + // Get all adoptions with filters @GET("api/v1/adoptions") Call> getAllAdoptions( @Query("page") int page, @@ -27,18 +29,23 @@ public interface AdoptionApi { @Query("employeeId") Long employeeId, @Query("sort") String sort); + // Get adoption by id @GET("api/v1/adoptions/{id}") Call getAdoptionById(@Path("id") Long id); + // Create adoption @POST("api/v1/adoptions") Call createAdoption(@Body AdoptionDTO adoption); + // Update adoption @PUT("api/v1/adoptions/{id}") Call updateAdoption(@Path("id") Long id, @Body AdoptionDTO adoption); + // Delete adoption @DELETE("api/v1/adoptions/{id}") Call deleteAdoption(@Path("id") Long id); + // Bulk delete adoptions @HTTP(method = "DELETE", path = "api/v1/adoptions", hasBody = true) Call bulkDeleteAdoptions(@Body BulkDeleteRequest request); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java index 5bc88643..f0752c00 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java @@ -14,8 +14,10 @@ import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; +// api calls for appointments public interface AppointmentApi { + // Get all appointments with filters @GET("api/v1/appointments") Call> getAllAppointments( @Query("page") int page, @@ -27,18 +29,23 @@ public interface AppointmentApi { @Query("employeeId") Long employeeId, @Query("sort") String sort); + // Get appointment by id @GET("api/v1/appointments/{id}") Call getAppointmentById(@Path("id") Long id); + // Create appointment @POST("api/v1/appointments") Call createAppointment(@Body AppointmentDTO appointment); + // Update appointment @PUT("api/v1/appointments/{id}") Call updateAppointment(@Path("id") Long id, @Body AppointmentDTO appointment); + // Delete appointment @DELETE("api/v1/appointments/{id}") Call deleteAppointment(@Path("id") Long id); + // Bulk delete appointments @HTTP(method = "DELETE", path = "api/v1/appointments", hasBody = true) Call bulkDeleteAppointments(@Body BulkDeleteRequest request); -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/CategoryApi.java b/android/app/src/main/java/com/example/petstoremobile/api/CategoryApi.java index d1084482..a82e0ab8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/CategoryApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/CategoryApi.java @@ -6,8 +6,10 @@ import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; +// api calls for categories public interface CategoryApi { + // Get all categories with pagination @GET("api/v1/categories") Call> getAllCategories( @Query("page") int page, diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java index 7a4ab393..8a818a0a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java @@ -12,15 +12,18 @@ import retrofit2.http.GET; import retrofit2.http.PUT; import retrofit2.http.Path; -//api calls to get conversations +// api calls for chat conversations public interface ChatApi { + // Get all conversations @GET("api/v1/chat/conversations") Call> getAllConversations(); + // Get conversation by id @GET("api/v1/chat/conversations/{conversationId}") Call getConversationById(@Path("conversationId") Long conversationId); + // Update conversation status @PUT("api/v1/chat/conversations/{conversationId}") Call updateConversationStatus(@Path("conversationId") Long conversationId, @Body UpdateConversationStatusRequest request); diff --git a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java index 55c51682..14be64e0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java @@ -15,23 +15,30 @@ import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; +// api calls for customers public interface CustomerApi { + // Get all customers with pagination @GET("api/v1/customers") Call> getAllCustomers(@Query("page") int page, @Query("size") int size); + // Get customer by id @GET("api/v1/customers/{customerId}") Call getCustomerById(@Path("customerId") Long customerId); + // Update customer @PUT("api/v1/customers/{customerId}") Call updateCustomer(@Path("customerId") Long customerId, @Body CustomerDTO customer); + // Delete customer @DELETE("api/v1/customers/{customerId}") Call deleteCustomer(@Path("customerId") Long customerId); + // Register customer @POST("api/v1/auth/register") Call registerCustomer(@Body CustomerDTO customer); + // Get customer dropdowns @GET("api/v1/dropdowns/customers") Call> getCustomerDropdowns(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/EmployeeApi.java b/android/app/src/main/java/com/example/petstoremobile/api/EmployeeApi.java index bbd873c3..33f08264 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/EmployeeApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/EmployeeApi.java @@ -5,28 +5,28 @@ import com.example.petstoremobile.dtos.PageResponse; import retrofit2.Call; import retrofit2.http.*; +// api calls for employees public interface EmployeeApi { + // Get all employees with pagination @GET("api/v1/employees") Call> getAllEmployees( @Query("page") int page, @Query("size") int size); + // Get employee by id @GET("api/v1/employees/{id}") Call getEmployeeById(@Path("id") Long id); + // Create employee @POST("api/v1/employees") Call createEmployee(@Body EmployeeDTO employee); + // Update employee @PUT("api/v1/employees/{id}") Call updateEmployee(@Path("id") Long id, @Body EmployeeDTO employee); + // Delete employee @DELETE("api/v1/employees/{id}") Call deleteEmployee(@Path("id") Long id); } - - - - - - diff --git a/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java index fa1e17b4..e9b3bb7c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java @@ -14,8 +14,10 @@ import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; +// api calls for inventory public interface InventoryApi { + // Get all inventory with filters @GET("api/v1/inventory") Call> getAllInventory( @Query("page") int page, @@ -24,23 +26,23 @@ public interface InventoryApi { @Query("storeId") Long storeId, @Query("sort") String sort); - // GET /api/v1/inventory/{id} + // Get inventory by id @GET("api/v1/inventory/{id}") Call getInventoryById(@Path("id") Long id); - // POST /api/v1/inventory + // Create inventory @POST("api/v1/inventory") Call createInventory(@Body InventoryDTO request); - // PUT /api/v1/inventory/{id} + // Update inventory @PUT("api/v1/inventory/{id}") Call updateInventory(@Path("id") Long id, @Body InventoryDTO request); - // DELETE /api/v1/inventory/{id} + // Delete inventory @DELETE("api/v1/inventory/{id}") Call deleteInventory(@Path("id") Long id); - // DELETE /api/v1/inventory (bulk delete) + // Bulk delete inventory @HTTP(method = "DELETE", path = "api/v1/inventory", hasBody = true) Call bulkDeleteInventory(@Body BulkDeleteRequest request); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java index e5504a03..cb921932 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java @@ -15,15 +15,18 @@ import retrofit2.http.Part; import retrofit2.http.Path; import retrofit2.http.Streaming; -//api calls to get and send messages +// api calls for messages public interface MessageApi { + // Get messages for a conversation @GET("api/v1/chat/conversations/{id}/messages") Call> getMessages(@Path("id") Long conversationId); + // Send a message @POST("api/v1/chat/conversations/{id}/messages") Call sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request); + // Send a message with attachment @Multipart @POST("api/v1/chat/conversations/{id}/attachments") Call sendMessageWithAttachment( @@ -32,6 +35,7 @@ public interface MessageApi { @Part MultipartBody.Part file ); + // Download attachment @GET("api/v1/chat/messages/{id}/attachment") @Streaming Call downloadAttachment(@Path("id") Long messageId); diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java index b554166b..06e522f0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -20,7 +20,7 @@ import retrofit2.http.Part; import retrofit2.http.Path; import retrofit2.http.Query; -//api calls to CRUD pets +// api calls to CRUD pets public interface PetApi { // endpoint for downloading the pet's image file String PET_IMAGE_PATH = "api/v1/pets/%d/image"; @@ -38,18 +38,23 @@ public interface PetApi { @Query("sort") String sort ); + // Get pets by customer id @GET("api/v1/dropdowns/customers/{customerId}/pets") Call> getCustomerPets(@Path("customerId") Long customerId); + // Get adoption pets @GET("api/v1/dropdowns/adoption-pets") Call> getAdoptionPets(@Query("storeId") Long storeId); + // Get pet dropdowns @GET("api/v1/dropdowns/pets") Call> getPetDropdowns(); + // Get pet species dropdowns @GET("api/v1/dropdowns/pet-species") Call> getPetSpeciesDropdowns(); + // Get pet breeds dropdowns @GET("api/v1/dropdowns/pet-breeds") Call> getPetBreedsDropdowns(@Query("species") String species); @@ -81,5 +86,4 @@ public interface PetApi { // Delete pet image @DELETE("api/v1/pets/{id}/image") Call deletePetImage(@Path("id") Long id); - } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java index 8aeb596e..88d7df0f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java @@ -9,9 +9,12 @@ import retrofit2.http.*; import java.util.List; +// api calls for products public interface ProductApi { + // endpoint for downloading the product's image file String PRODUCT_IMAGE_PATH = "api/v1/products/%d/image"; + // Get all products with filters @GET("api/v1/products") Call> getAllProducts( @Query("q") String query, @@ -20,28 +23,36 @@ public interface ProductApi { @Query("size") int size, @Query("sort") String sort); + // Get product by id @GET("api/v1/products/{id}") Call getProductById(@Path("id") Long id); + // Create product @POST("api/v1/products") Call createProduct(@Body ProductDTO product); + // Update product @PUT("api/v1/products/{id}") Call updateProduct(@Path("id") Long id, @Body ProductDTO product); + // Delete product @DELETE("api/v1/products/{id}") Call deleteProduct(@Path("id") Long id); + // Upload product image @Multipart @POST("api/v1/products/{id}/image") Call uploadProductImage(@Path("id") Long id, @Part MultipartBody.Part image); + // Delete product image @DELETE("api/v1/products/{id}/image") Call deleteProductImage(@Path("id") Long id); + // Get product dropdowns @GET("api/v1/dropdowns/products") Call> getProductDropdowns(); + // Get category dropdowns @GET("api/v1/dropdowns/categories") Call> getCategoryDropdowns(); -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java index b4414be5..03f6d891 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java @@ -6,8 +6,10 @@ import com.example.petstoremobile.dtos.ProductSupplierDTO; import retrofit2.Call; import retrofit2.http.*; +// api calls for product-supplier relationships public interface ProductSupplierApi { + // Get all product-suppliers with filters @GET("api/v1/product-suppliers") Call> getAllProductSuppliers( @Query("page") int page, @@ -17,24 +19,30 @@ public interface ProductSupplierApi { @Query("supplierId") Long supplierId, @Query("sort") String sort); + // Get product-supplier by composite id @GET("api/v1/product-suppliers/{productId}/{supplierId}") Call getProductSupplierById( @Path("productId") Long productId, @Path("supplierId") Long supplierId); + // Create product-supplier @POST("api/v1/product-suppliers") Call createProductSupplier(@Body ProductSupplierDTO dto); + // Update product-supplier @PUT("api/v1/product-suppliers/{productId}/{supplierId}") Call updateProductSupplier( @Path("productId") Long productId, @Path("supplierId") Long supplierId, @Body ProductSupplierDTO dto); + // Delete product-supplier @DELETE("api/v1/product-suppliers/{productId}/{supplierId}") Call deleteProductSupplier( @Path("productId") Long productId, @Path("supplierId") Long supplierId); + + // Bulk delete product-suppliers @HTTP(method = "DELETE", path = "api/v1/product-suppliers", hasBody = true) Call bulkDeleteProductSuppliers(@Body BulkDeleteRequest request); -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java index ebb99139..8746ed08 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java @@ -7,8 +7,10 @@ import retrofit2.http.GET; import retrofit2.http.Path; import retrofit2.http.Query; +// api calls for purchase orders public interface PurchaseOrderApi { + // Get all purchase orders with filters @GET("api/v1/purchase-orders") Call> getAllPurchaseOrders( @Query("page") int page, @@ -17,6 +19,7 @@ public interface PurchaseOrderApi { @Query("storeId") Long storeId, @Query("sort") String sort); + // Get purchase order by id @GET("api/v1/purchase-orders/{id}") Call getPurchaseOrderById(@Path("id") Long id); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RefundApi.java b/android/app/src/main/java/com/example/petstoremobile/api/RefundApi.java index d7ba9575..6e3591bc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/RefundApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/RefundApi.java @@ -6,20 +6,26 @@ import retrofit2.http.*; import java.util.List; +// api calls for refunds public interface RefundApi { + // Get all refunds @GET("api/v1/refunds") Call> getAllRefunds(); + // Get refund by id @GET("api/v1/refunds/{id}") Call getRefundById(@Path("id") Long id); + // Create refund @POST("api/v1/refunds") Call createRefund(@Body RefundDTO refund); + // Update refund @PUT("api/v1/refunds/{id}") Call updateRefund(@Path("id") Long id, @Body RefundDTO refund); + // Delete refund @DELETE("api/v1/refunds/{id}") Call deleteRefund(@Path("id") Long id); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java index cec94044..6d333dd5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java @@ -10,8 +10,10 @@ import retrofit2.http.POST; import retrofit2.http.Path; import retrofit2.http.Query; +// api calls for sales public interface SaleApi { + // Get all sales with filters @GET("api/v1/sales") Call> getAllSales( @Query("page") int page, @@ -23,9 +25,11 @@ public interface SaleApi { @Query("customerId") Long customerId, @Query("sort") String sort); + // Get sale by id @GET("api/v1/sales/{id}") Call getSaleById(@Path("id") Long id); + // Create sale @POST("api/v1/sales") Call createSale(@Body SaleDTO sale); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java index 43014a53..9784524b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java @@ -14,7 +14,7 @@ import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; -//api calls to CRUD services +// api calls to CRUD services public interface ServiceApi { // Get all services @GET("api/v1/services") diff --git a/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java b/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java index f71b92b6..3370dd03 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/StoreApi.java @@ -11,16 +11,20 @@ import retrofit2.http.GET; import retrofit2.http.Path; import retrofit2.http.Query; +// api calls for stores public interface StoreApi { + // Get all stores with pagination @GET("api/v1/stores") Call> getAllStores( @Query("page") int page, @Query("size") int size); + // Get store dropdowns @GET("api/v1/dropdowns/stores") Call> getStoreDropdowns(); + // Get employees of a specific store @GET("api/v1/dropdowns/stores/{storeId}/employees") Call> getStoreEmployees(@Path("storeId") Long storeId); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java b/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java index 8346e221..dbc5640b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java @@ -7,9 +7,12 @@ import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; +// api calls for users public interface UserApi { + // endpoint for downloading the user's avatar file String AVATAR_PATH = "api/v1/users/%d/avatar/file"; + // Get all users with filters @GET("api/v1/users") Call> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java index dc6096de..f28395d3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java @@ -8,6 +8,7 @@ import javax.inject.Singleton; import dagger.hilt.android.qualifiers.ApplicationContext; +//Used to save and retrieve login data @Singleton public class TokenManager { private static final String TOKEN_KEY = "token"; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ActivityLogDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ActivityLogDTO.java index 2d4d0f6d..4a210a5d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/ActivityLogDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ActivityLogDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for activity logs. + */ public class ActivityLogDTO { private Long logId; private String activity; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java index d48bc942..66459bee 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java @@ -2,6 +2,9 @@ package com.example.petstoremobile.dtos; import java.math.BigDecimal; +/** + * Data Transfer Object for pet adoptions. + */ public class AdoptionDTO { private Long adoptionId; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java index 37f6640f..c793a142 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for appointments. + */ public class AppointmentDTO { private Long appointmentId; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java index 6aecdbc3..563b1b5f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java @@ -1,6 +1,8 @@ package com.example.petstoremobile.dtos; -//Used to send login data to the backend +/** + * Data Transfer Object for authentication credentials. + */ public class AuthDTO { public static class LoginRequest { private String username; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AvatarUploadResponse.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AvatarUploadResponse.java index 194be1f4..4495e05e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AvatarUploadResponse.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AvatarUploadResponse.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Response containing the URL of a newly uploaded avatar. + */ public class AvatarUploadResponse { private String avatarUrl; private String message; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java index e53c8369..0fd6ae5a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/BulkDeleteRequest.java @@ -2,6 +2,9 @@ package com.example.petstoremobile.dtos; import java.util.List; +/** + * Request body for deleting multiple records at once. + */ public class BulkDeleteRequest { private List ids; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CategoryDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CategoryDTO.java index 6596845b..b8c84b7c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/CategoryDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CategoryDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for product categories. + */ public class CategoryDTO { private Long categoryId; private String categoryName; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java index 3a7ea42e..135bd84d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for chat conversations. + */ public class ConversationDTO { private Long id; private Long customerId; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CouponDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CouponDTO.java index e2ecb2c4..647d9baf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/CouponDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CouponDTO.java @@ -2,6 +2,9 @@ package com.example.petstoremobile.dtos; import java.math.BigDecimal; +/** + * Data Transfer Object for coupons. + */ public class CouponDTO { private Long couponId; private String couponCode; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java index c190342e..0872252c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java @@ -2,6 +2,9 @@ package com.example.petstoremobile.dtos; import com.google.gson.annotations.SerializedName; +/** + * Data Transfer Object for customers. + */ public class CustomerDTO { @SerializedName("id") private Long customerId; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/DropdownDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/DropdownDTO.java index 3174dae5..3f46d119 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/DropdownDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/DropdownDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for simple dropdown selection lists. + */ public class DropdownDTO { private Long id; private String label; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java index f29b4beb..f7288d73 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/EmployeeDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for employees. + */ public class EmployeeDTO { private Long id; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ErrorResponse.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ErrorResponse.java index ddb23eaa..56cc726c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/ErrorResponse.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ErrorResponse.java @@ -1,7 +1,8 @@ package com.example.petstoremobile.dtos; -//Used to get messages of any errors from the backend - +/** + * Used to get messages of any errors from the backend. + */ public class ErrorResponse { private String message; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java index ddafd045..3fd6be57 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for inventory stock. + */ public class InventoryDTO { // Response fields (from backend InventoryResponse) private Long inventoryId; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java index 8302ba86..17b54fa8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java @@ -2,6 +2,9 @@ package com.example.petstoremobile.dtos; import com.google.gson.annotations.SerializedName; +/** + * Data Transfer Object for chat messages. + */ public class MessageDTO { @SerializedName("id") diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/PageResponse.java b/android/app/src/main/java/com/example/petstoremobile/dtos/PageResponse.java index 7237105e..56558e15 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/PageResponse.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/PageResponse.java @@ -2,7 +2,9 @@ package com.example.petstoremobile.dtos; import java.util.List; -//Used to get data from the API +/** + * Generic response wrapper for paginated API results. + */ public class PageResponse { private List content; private int totalPages; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java index 0e9a0b3f..3a3314d5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object representing a pet. + */ public class PetDTO { private Long petId; private String petName; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ProductDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ProductDTO.java index 2b016b28..c95f0a32 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/ProductDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ProductDTO.java @@ -2,6 +2,9 @@ package com.example.petstoremobile.dtos; import java.math.BigDecimal; +/** + * Data Transfer Object for products. + */ public class ProductDTO { private Long prodId; private String prodName; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ProductSupplierDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ProductSupplierDTO.java index 887d29e1..e25f82ec 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/ProductSupplierDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ProductSupplierDTO.java @@ -2,6 +2,9 @@ package com.example.petstoremobile.dtos; import java.math.BigDecimal; +/** + * Data Transfer Object for mapping products to suppliers. + */ public class ProductSupplierDTO { private Long productId; private String productName; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/PurchaseOrderDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/PurchaseOrderDTO.java index 813633c9..eb760e59 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/PurchaseOrderDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/PurchaseOrderDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for purchase orders. + */ public class PurchaseOrderDTO { private Long purchaseOrderId; private Long supId; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/RefundDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/RefundDTO.java index 5f47e1ce..dc3d2d67 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/RefundDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/RefundDTO.java @@ -2,6 +2,9 @@ package com.example.petstoremobile.dtos; import java.math.BigDecimal; +/** + * Data Transfer Object for refund processing. + */ public class RefundDTO { // Response fields private Long id; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java index 9dc76029..d8501995 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java @@ -3,6 +3,9 @@ package com.example.petstoremobile.dtos; import java.math.BigDecimal; import java.util.List; +/** + * Data Transfer Object for sales transactions. + */ public class SaleDTO { // Response fields private Long saleId; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java index 7a521de3..aab51a90 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Request body for sending a new chat message. + */ public class SendMessageRequest { private String content; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ServiceDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ServiceDTO.java index 56e44371..cfbacb8e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/ServiceDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ServiceDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for services. + */ public class ServiceDTO { private Long serviceId; private String serviceName; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/StoreDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/StoreDTO.java index da66f046..ba4d732a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/StoreDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/StoreDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for store information. + */ public class StoreDTO { private Long storeId; private String storeName; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SupplierDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SupplierDTO.java index e34816c1..e39d22c8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/SupplierDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SupplierDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for suppliers. + */ public class SupplierDTO { private Long supId; private String supCompany; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/UpdateConversationStatusRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/UpdateConversationStatusRequest.java index 4d7987e1..e0fe3fe2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/UpdateConversationStatusRequest.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/UpdateConversationStatusRequest.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Request body for updating chat conversation status. + */ public class UpdateConversationStatusRequest { private String status; diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/UserDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/UserDTO.java index 2ab9902e..05866080 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/UserDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/UserDTO.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.dtos; +/** + * Data Transfer Object for user account details. + */ public class UserDTO { private Long id; private String username; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index 88bb0f09..83678a68 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -65,6 +65,9 @@ import okhttp3.MultipartBody; import okhttp3.RequestBody; import okhttp3.ResponseBody; +/** + * Fragment for handling customer support chat. + */ @AndroidEntryPoint public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener, StompChatManager.ConversationListener, StompChatManager.ConnectionListener { @@ -90,6 +93,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private StompChatManager stompChatManager; private ActivityResultLauncher attachmentLauncher; + /** + * Initializes the view model and attachment launcher. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -105,6 +111,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis ); } + /** + * Inflates the layout and sets up UI event listeners. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -137,6 +146,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis return binding.getRoot(); } + /** + * Sets up the logic to open and close the chat drawer. + */ private void setupDrawerToggles() { binding.headerActiveChats.setOnClickListener(v -> { if (binding.rvActiveChats.getVisibility() == View.VISIBLE) { @@ -159,6 +171,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } + /** + * Configures the adapters and layout managers for chat lists and message history. + */ private void setupRecyclerViews() { activeChatAdapter = new ChatAdapter(activeChatList, this); binding.rvActiveChats.setLayoutManager(new LinearLayoutManager(getContext())); @@ -187,6 +202,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis setConversationActive(false, null); } + /** + * Displays a full-screen image preview for message attachments. + */ private void showFullScreenImage(Message message) { if (baseUrl == null || message.getId() == null) return; @@ -208,6 +226,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis dialog.show(); } + /** + * Initiates the download process for a message attachment. + */ private void downloadFile(Message message) { if (message.getId() == null) return; @@ -227,6 +248,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } + /** + * Saves the downloaded file body to the device's downloads folder. + */ private void saveFileToDownloads(ResponseBody body, String fileName, String mimeType) { android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper()); new Thread(() -> { @@ -270,6 +294,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }).start(); } + /** + * Observes LiveData from the ViewModel to update chat lists and messages. + */ private void observeViewModel() { viewModel.getActiveChats().observe(getViewLifecycleOwner(), list -> { activeChatList.clear(); @@ -300,12 +327,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis viewModel.getIsLoading().observe(getViewLifecycleOwner(), this::setLoading); } + /** + * Toggles the visibility of the progress bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Updates the chat header and input state if the active conversation changes. + */ private void updateTitleAndStateIfActive(List list) { if (activeConversationId != null) { for (Chat chat : list) { @@ -318,6 +351,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } + /** + * Loads initial chat data and establishes WebSocket connection. + */ private void loadInitialData() { String token = tokenManager.getToken(); Long currentUserId = tokenManager.getUserId(); @@ -353,6 +389,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } + /** + * Handles clicks on a chat from the drawer to switch the active conversation. + */ @Override public void onChatClick(Chat chat) { activeConversationId = Long.parseLong(chat.getChatId()); @@ -368,6 +407,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis viewModel.loadMessageHistory(activeConversationId); } + /** + * Closes the active chat conversation. + */ private void closeChat() { if (activeConversationId == null) return; @@ -392,6 +434,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } + /** + * Sends a text message to the active conversation. + */ private void sendMessage() { if (activeConversationId == null) return; String text = binding.etMessage.getText().toString().trim(); @@ -408,12 +453,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } + /** + * Opens a file picker to select an attachment. + */ private void selectAttachment() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); attachmentLauncher.launch(intent); } + /** + * Displays a preview of the selected attachment. + */ private void showAttachmentPreview(Uri uri) { pendingAttachmentUri = uri; binding.layoutAttachmentPreview.setVisibility(View.VISIBLE); @@ -427,11 +478,17 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } + /** + * Removes the currently selected attachment from the preview. + */ private void removeAttachment() { pendingAttachmentUri = null; binding.layoutAttachmentPreview.setVisibility(View.GONE); } + /** + * Sends a message with a file attachment. + */ private void sendWithAttachment(Uri uri) { if (activeConversationId == null) return; @@ -468,6 +525,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } + /** + * Callback for when a new message is received through the WebSocket. + */ @Override public void onMessageReceived(MessageDTO dto) { requireActivity().runOnUiThread(() -> { @@ -478,6 +538,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } + /** + * Callback for when a conversation's status or last message is updated. + */ @Override public void onConversationUpdated(ConversationDTO dto) { requireActivity().runOnUiThread(() -> { @@ -489,6 +552,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } + /** + * Callback for when the WebSocket connection is successfully opened. + */ @Override public void onSocketOpened() { if (!isAdded()) return; @@ -498,12 +564,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } + /** + * Callback for when the WebSocket connection is closed. + */ @Override public void onSocketClosed() { if (!isAdded()) return; requireActivity().runOnUiThread(viewModel::loadConversations); } + /** + * Callback for when a WebSocket error occurs. + */ @Override public void onSocketError() { if (!isAdded()) return; @@ -513,6 +585,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } + /** + * Scrolls the message list to the most recent message. + */ private void scrollToBottom() { if (!messageList.isEmpty()) { binding.rvMessages.post(() -> @@ -520,6 +595,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } + /** + * Updates the UI state based on whether a conversation is active and its status. + */ private void setConversationActive(boolean active, String status) { boolean isClosed = "CLOSED".equalsIgnoreCase(status); UIUtils.setViewsEnabled(active && !isClosed, binding.btnSend, binding.etMessage, binding.btnAttach); @@ -541,6 +619,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } + /** + * Cleans up resources and disconnects the WebSocket when the view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java index 6f2c643b..8c3f3c2e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java @@ -23,7 +23,9 @@ import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; -//The Fragment for the displaying the list of entities to be viewed +/** + * Fragment that serves as a container for various list-based screens, providing a navigation drawer. + */ @AndroidEntryPoint public class ListFragment extends Fragment { @@ -97,6 +99,9 @@ public class ListFragment extends Fragment { return binding.getRoot(); } + /** + * Cleans up the binding when the view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index 7f8fc039..fb1decb7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -173,12 +173,18 @@ public class ProfileFragment extends Fragment { return binding.getRoot(); } + /** + * Toggles the visibility of the progress bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Cleans up the binding when the view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java index f369d383..6dab8ec3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java @@ -34,6 +34,9 @@ import java.util.List; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for viewing application activity logs with various filtering options. + */ @AndroidEntryPoint public class ActivityLogFragment extends Fragment { private FragmentActivityLogBinding binding; @@ -46,6 +49,9 @@ public class ActivityLogFragment extends Fragment { @Inject TokenManager tokenManager; + /** + * Inflates the layout, checks for admin access, and initializes ViewModel and UI components. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -69,12 +75,18 @@ public class ActivityLogFragment extends Fragment { return binding.getRoot(); } + /** + * Triggers initial data loading after the view is created. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); viewModel.loadInitialData(); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new ActivityLogAdapter(logList); binding.recyclerViewActivityLog.setLayoutManager(new LinearLayoutManager(getContext())); @@ -97,6 +109,9 @@ public class ActivityLogFragment extends Fragment { }); } + /** + * Sets up filters for logs, including search, role, store, and date range. + */ private void setupFilters() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchLog, binding.spinnerRoleFilter, binding.spinnerStoreFilter); @@ -123,6 +138,9 @@ public class ActivityLogFragment extends Fragment { }); } + /** + * Displays a date picker dialog and updates the selected start or end date. + */ private void showDatePicker(boolean isStart) { Calendar cal = Calendar.getInstance(); new DatePickerDialog(requireContext(), (view, year, month, day) -> { @@ -141,12 +159,18 @@ public class ActivityLogFragment extends Fragment { }, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show(); } + /** + * Handles store selection from the filter spinner. + */ private void onStoreSelected() { int pos = binding.spinnerStoreFilter.getSelectedItemPosition(); Long storeId = (pos > 0 && !storeList.isEmpty()) ? storeList.get(pos - 1).getId() : null; viewModel.setStoreFilter(storeId); } + /** + * Observes the ViewModel for log list updates, store options, and loading status. + */ private void observeViewModel() { viewModel.getLogs().observe(getViewLifecycleOwner(), list -> { logList.clear(); @@ -164,6 +188,9 @@ public class ActivityLogFragment extends Fragment { binding.swipeRefreshActivityLog.setRefreshing(loading)); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index a6ba396e..fd4209f1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -44,6 +44,9 @@ import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and managing a list of adoptions with a calendar view and filtering. + */ @AndroidEntryPoint public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener { @@ -58,12 +61,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop @Inject TokenManager tokenManager; + /** + * Initializes the ViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(AdoptionListViewModel.class); } + /** + * Inflates the layout and sets up UI components, calendar, and observers. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -88,6 +97,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop return binding.getRoot(); } + /** + * Observes the ViewModel for adoption list, stores, and loading status. + */ private void observeViewModel() { viewModel.getAdoptions().observe(getViewLifecycleOwner(), list -> { adoptionList.clear(); @@ -106,6 +118,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop }); } + /** + * Configures the bulk delete handler for multiple adoption record deletion. + */ private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -119,6 +134,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop ); } + /** + * Reloads adoption data and stores when the fragment resumes. + */ @Override public void onResume() { super.onResume(); @@ -126,6 +144,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop if (!isStaff()) viewModel.loadStores(); } + /** + * Toggles between month and week display modes for the calendar. + */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; binding.calendarViewAdoption.state().edit() @@ -133,6 +154,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop .commit(); } + /** + * Sets up the filter visibility toggle, considering user roles. + */ private void setupFilterToggle() { if (isStaff()) { UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption, @@ -144,10 +168,16 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop } } + /** + * Checks if the currently logged-in user has the STAFF role. + */ private boolean isStaff() { return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } + /** + * Configures the calendar view for date-based filtering. + */ private void setupCalendar() { binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> { if (selected) { @@ -164,6 +194,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop }); } + /** + * Updates calendar decorators to highlight dates with adoptions. + */ private void updateCalendarDecorators() { HashSet datesWithAdoptions = new HashSet<>(); for (AdoptionDTO adoption : adoptionList) { @@ -184,6 +217,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions)); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new AdoptionAdapter(adoptionList, this); binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext())); @@ -206,23 +242,38 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop }); } + /** + * Sets up the search input listener. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchAdoption, () -> loadAdoptions(true)); } + /** + * Configures the status filter spinner. + */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Completed", "Pending", "Missed", "Cancelled"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, () -> loadAdoptions(true)); } + /** + * Configures the store filter spinner. + */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, () -> loadAdoptions(true)); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshAdoption.setOnRefreshListener(() -> loadAdoptions(true)); } + /** + * Loads adoption data based on current filters, search query, and selected date. + */ private void loadAdoptions(boolean reset) { String query = binding.etSearchAdoption.getText().toString().trim(); String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses"; @@ -250,6 +301,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop viewModel.loadAdoptions(reset, query, status, storeId, selectedDateString, null); } + /** + * Navigates to the adoption detail screen. + */ private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -259,9 +313,15 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args); } + /** + * Handles adoption item clicks by opening details. + */ @Override public void onAdoptionClick(int position) { openDetail(position); } + /** + * Forwards selection changes to the bulk delete handler. + */ @Override public void onSelectionChanged(int selectedCount) { if (bulkDeleteHandler != null) { @@ -269,6 +329,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop } } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java index 89aeb098..e493b0e3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -18,6 +18,9 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; +/** + * Fragment for displaying business analytics, including revenue, transactions, and product performance. + */ @AndroidEntryPoint public class AnalyticsFragment extends Fragment { @@ -31,6 +34,9 @@ public class AnalyticsFragment extends Fragment { private static final String[] TOP_N_OPTIONS = {"5", "10", "15", "20"}; private static final int[] TOP_N_VALUES = { 5, 10, 15, 20 }; + /** + * Inflates the layout, initializes ViewModel, and sets up UI components and filters. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -51,6 +57,9 @@ public class AnalyticsFragment extends Fragment { private static final int COLOR_SELECTED = 0xFF4ECDC4; private static final int COLOR_UNSELECTED = 0xFFCBD5E1; + /** + * Configures the view mode toggle buttons (My Analytics vs Store Analytics). + */ private void setupViewModeToggle() { updateViewModeButtonStyles(viewModel.getViewMode()); @@ -67,6 +76,9 @@ public class AnalyticsFragment extends Fragment { }); } + /** + * Updates the styles of the view mode buttons based on the selected mode. + */ private void updateViewModeButtonStyles(String mode) { binding.btnMyAnalytics.setBackgroundTintList( android.content.res.ColorStateList.valueOf(mode.equals("mine") ? COLOR_SELECTED : COLOR_UNSELECTED)); @@ -74,6 +86,9 @@ public class AnalyticsFragment extends Fragment { android.content.res.ColorStateList.valueOf(mode.equals("store") ? COLOR_SELECTED : COLOR_UNSELECTED)); } + /** + * Updates the visibility of the store filter based on the user's role and selected view mode. + */ private void updateStoreFilterVisibility(String mode) { boolean isAdmin = "ADMIN".equalsIgnoreCase(tokenManager.getRole()); int vis = (isAdmin && mode.equals("store")) ? View.VISIBLE : View.GONE; @@ -81,8 +96,10 @@ public class AnalyticsFragment extends Fragment { binding.spinnerFilterStore.setVisibility(vis); } - // Filter Panel + /** + * Configures the filter panel, including date pickers, presets, and action buttons. + */ private void setupFilterPanel() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerTopN, TOP_N_OPTIONS); @@ -111,12 +128,18 @@ public class AnalyticsFragment extends Fragment { binding.btnFilterReset.setOnClickListener(v -> resetFilters()); } + /** + * Toggles the visibility of the filter content section. + */ private void toggleFilters() { filtersExpanded = !filtersExpanded; binding.llFilterContent.setVisibility(filtersExpanded ? View.VISIBLE : View.GONE); binding.tvFilterToggleIcon.setText(filtersExpanded ? "▲" : "▼"); } + /** + * Applies a date range preset to the filter fields. + */ private void applyPreset(int startOffset, int endOffset) { binding.etFilterStartDate.setText(getDateString(startOffset)); binding.etFilterEndDate.setText(getDateString(endOffset)); @@ -124,6 +147,9 @@ public class AnalyticsFragment extends Fragment { applyFiltersFromUI(); } + /** + * Reads filter values from the UI and applies them to the ViewModel. + */ private void applyFiltersFromUI() { AnalyticsViewModel.FilterState filter = new AnalyticsViewModel.FilterState(); filter.startDate = binding.etFilterStartDate.getText().toString().trim(); @@ -142,6 +168,9 @@ public class AnalyticsFragment extends Fragment { viewModel.applyFilter(filter); } + /** + * Resets all filters to their default values. + */ private void resetFilters() { binding.etFilterStartDate.setText(""); binding.etFilterEndDate.setText(""); @@ -152,6 +181,9 @@ public class AnalyticsFragment extends Fragment { viewModel.resetFilter(); } + /** + * Updates the text summary of the currently selected date range. + */ private void updateFilterSummary() { String start = binding.etFilterStartDate.getText().toString().trim(); String end = binding.etFilterEndDate.getText().toString().trim(); @@ -166,10 +198,16 @@ public class AnalyticsFragment extends Fragment { } } + /** + * Formats a date string into a shorter version for display. + */ private String shortDate(String date) { return (date != null && date.length() >= 10) ? date.substring(5) : date; } + /** + * Returns a formatted date string for a given day. + */ private String getDateString(int offsetDays) { Calendar c = Calendar.getInstance(); c.add(Calendar.DAY_OF_YEAR, offsetDays); @@ -177,8 +215,10 @@ public class AnalyticsFragment extends Fragment { c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH)); } - // ViewModel Observation + /** + * Observes the ViewModel for analytics data, loading status, errors, and filter options. + */ private void observeViewModel() { viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay); @@ -216,14 +256,19 @@ public class AnalyticsFragment extends Fragment { }); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } - // Display + /** + * Computes and displays analytics data in summary cards and bar charts. + */ private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) { if (data == null) return; @@ -312,8 +357,10 @@ public class AnalyticsFragment extends Fragment { } } - // Chart Helpers + /** + * Dynamically adds a bar chart row to a given layout container. + */ private void addBarRow(LinearLayout parent, String label, String value, float ratio, String color) { if (getContext() == null) return; LinearLayout row = new LinearLayout(getContext()); @@ -359,6 +406,9 @@ public class AnalyticsFragment extends Fragment { parent.addView(row); } + /** + * Adds an empty message row to a given layout container. + */ private void addEmptyRow(LinearLayout parent, String message) { if (getContext() == null) return; TextView tv = new TextView(getContext()); @@ -368,6 +418,9 @@ public class AnalyticsFragment extends Fragment { parent.addView(tv); } + /** + * Displays an error message and updates UI to reflect the error state. + */ private void showError(String msg) { if (getContext() == null || binding == null) return; binding.tvTotalRevenue.setText("Error"); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index b8196775..99530081 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -45,6 +45,9 @@ import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and managing a list of appointments with calendar integration and filtering. + */ @AndroidEntryPoint public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener { @@ -63,6 +66,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private Long currentUserId = null; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + /** + * Initializes ViewModels for appointment and authentication data. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -70,6 +76,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. authViewModel = new ViewModelProvider(this).get(AuthViewModel.class); } + /** + * Inflates the layout and sets up UI components, calendar, and observers. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -97,6 +106,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. return binding.getRoot(); } + /** + * Observes the ViewModel for appointment list, stores, and loading status. + */ private void observeViewModel() { viewModel.getAppointments().observe(getViewLifecycleOwner(), list -> { appointmentList.clear(); @@ -115,6 +127,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } + /** + * Configures the bulk delete handler for multiple appointment deletion. + */ private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -128,6 +143,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. ); } + /** + * Reloads appointment data and stores when the fragment resumes. + */ @Override public void onResume() { super.onResume(); @@ -135,6 +153,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. if (!isStaff()) viewModel.loadStores(); } + /** + * Toggles between month and week display modes for the calendar. + */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; binding.calendarView.state().edit() @@ -142,12 +163,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. .commit(); } + /** + * Sets up the "My Appointments" filter button. + */ private void setupMyAppointmentFilter() { binding.btnMyAppointments.setOnClickListener(v -> { loadAppointmentData(true); }); } + /** + * Loads information about the currently logged-in user. + */ private void loadCurrentUserInfo() { authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -156,6 +183,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } + /** + * Sets up the filter visibility toggle. + */ private void setupFilterToggle() { if (isStaff()) { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment, binding.spinnerStatus); @@ -166,6 +196,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } } + /** + * Configures the calendar view for date-based filtering. + */ private void setupCalendar() { binding.calendarView.setOnDateChangedListener((widget, date, selected) -> { if (selected) { @@ -182,6 +215,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } + /** + * Updates calendar decorators to highlight dates with appointments. + */ private void updateCalendarDecorators() { HashSet datesWithAppointments = new HashSet<>(); for (AppointmentDTO appointment : appointmentList) { @@ -200,23 +236,38 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments)); } + /** + * Sets up the search input listener. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchAppointment, () -> loadAppointmentData(true)); } + /** + * Configures the status filter spinner. + */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, () -> loadAppointmentData(true)); } + /** + * Configures the store filter spinner. + */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadAppointmentData(true)); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshAppointment.setOnRefreshListener(() -> loadAppointmentData(true)); } + /** + * Navigates to the appointment detail screen. + */ private void openAppointmentDetails(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -226,11 +277,17 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args); } + /** + * Handles appointment item clicks by opening details. + */ @Override public void onAppointmentClick(int position) { openAppointmentDetails(position); } + /** + * Forwards selection changes to the bulk delete handler. + */ @Override public void onSelectionChanged(int count) { if (bulkDeleteHandler != null) { @@ -238,10 +295,16 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } } + /** + * Checks if the currently logged-in user has the STAFF role. + */ private boolean isStaff() { return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } + /** + * Loads appointment data based on current filters, search query, and selected date. + */ private void loadAppointmentData(boolean reset) { String query = binding.etSearchAppointment.getText().toString().trim(); String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; @@ -274,6 +337,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. viewModel.loadAppointments(reset, query, status, storeId, selectedDateString, employeeId); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new AppointmentAdapter(appointmentList, this); binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext())); @@ -296,6 +362,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java index c0beebfe..9bc754b4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CouponFragment.java @@ -27,6 +27,9 @@ import java.util.List; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and managing a list of coupons. + */ @AndroidEntryPoint public class CouponFragment extends Fragment implements CouponAdapter.OnCouponClickListener { private FragmentCouponBinding binding; @@ -34,6 +37,9 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl private CouponAdapter adapter; private final List couponList = new ArrayList<>(); + /** + * Inflates the layout, initializes ViewModel, and sets up UI components. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -59,6 +65,9 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl return binding.getRoot(); } + /** + * Observes the ViewModel for coupon list updates and loading status. + */ private void observeViewModel() { viewModel.getCoupons().observe(getViewLifecycleOwner(), list -> { couponList.clear(); @@ -71,6 +80,9 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl }); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new CouponAdapter(couponList, this); binding.recyclerViewCoupon.setLayoutManager(new LinearLayoutManager(getContext())); @@ -93,24 +105,39 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl }); } + /** + * Configures the status filter spinner. + */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Active", "Inactive"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCoupon, statuses, () -> applyFilters(true)); } + /** + * Configures the discount type filter spinner. + */ private void setupTypeFilter() { String[] types = {"All Types", "FIXED", "PERCENT"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerTypeCoupon, types, () -> applyFilters(true)); } + /** + * Sets up the search input listener. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchCoupon, () -> applyFilters(true)); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshCoupon.setOnRefreshListener(() -> applyFilters(true)); } + /** + * Applies filters and loads the coupon list. + */ private void applyFilters(boolean reset) { String statusStr = binding.spinnerStatusCoupon.getSelectedItem() != null ? binding.spinnerStatusCoupon.getSelectedItem().toString() : "All Statuses"; @@ -125,22 +152,34 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl viewModel.loadCoupons(reset, active, discountType, null); } + /** + * Navigates to the coupon detail screen. + */ private void openDetail(long id) { Bundle args = new Bundle(); args.putLong("couponId", id); Navigation.findNavController(requireView()).navigate(R.id.couponDetailFragment, args); } + /** + * Handles coupon item clicks by opening details. + */ @Override public void onCouponClick(CouponDTO coupon) { openDetail(coupon.getCouponId()); } + /** + * Shows or hides the bulk delete button based on selection count. + */ @Override public void onSelectionChanged(int count) { binding.btnBulkDeleteCoupons.setVisibility(count > 0 ? View.VISIBLE : View.GONE); } + /** + * Displays a confirmation dialog for deleting multiple coupons. + */ private void confirmBulkDelete() { new AlertDialog.Builder(requireContext()) .setTitle("Confirm Bulk Delete") diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java index 5afe4d1f..87155463 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/CustomerFragment.java @@ -22,6 +22,9 @@ import java.util.*; import javax.inject.Inject; import javax.inject.Named; +/** + * Fragment for displaying and managing a list of customers. + */ @AndroidEntryPoint public class CustomerFragment extends Fragment implements CustomerAdapter.OnCustomerClickListener { @@ -33,6 +36,9 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; + /** + * Inflates the layout, initializes ViewModel, and sets up UI components and filters. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -56,6 +62,9 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust return binding.getRoot(); } + /** + * Observes the ViewModel for customer list updates and loading status. + */ private void observeViewModel() { viewModel.getFilteredCustomers().observe(getViewLifecycleOwner(), list -> { customerList.clear(); @@ -68,6 +77,9 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust }); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new CustomerAdapter(customerList, this); adapter.setBaseUrl(baseUrl); @@ -92,15 +104,24 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust }); } + /** + * Configures the status filter spinner. + */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Active", "Inactive"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCustomer, statuses, this::applyFilters); } + /** + * Sets up the search input listener. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchCustomer, this::applyFilters); } + /** + * Applies filters and triggers data reloading or filtering in ViewModel. + */ private void applyFilters() { String query = binding.etSearchCustomer.getText().toString().trim(); String status = binding.spinnerStatusCustomer.getSelectedItem() != null ? @@ -108,10 +129,16 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust viewModel.filter(query, status); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshCustomer.setOnRefreshListener(() -> viewModel.loadCustomers(true)); } + /** + * Navigates to the customer detail screen. + */ private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -129,11 +156,17 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust NavHostFragment.findNavController(this).navigate(R.id.nav_customer_detail, args); } + /** + * Handles customer item clicks by opening details. + */ @Override public void onCustomerClick(int position) { openDetail(position); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 4e2c2337..c01fb710 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -32,6 +32,9 @@ import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and managing inventory items. + */ @AndroidEntryPoint public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener { @@ -43,12 +46,18 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn @Inject TokenManager tokenManager; + /** + * Initializes the ViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(InventoryListViewModel.class); } + /** + * Inflates the layout and sets up UI components, filters, and observers. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -71,6 +80,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn return binding.getRoot(); } + /** + * Observes the ViewModel for inventory list updates, store list, and loading status. + */ private void observeViewModel() { viewModel.getInventory().observe(getViewLifecycleOwner(), list -> { inventoryList.clear(); @@ -88,6 +100,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn }); } + /** + * Configures the bulk delete handler for multiple inventory item deletion. + */ private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -101,18 +116,27 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn ); } + /** + * Reloads store data if necessary when the fragment resumes. + */ @Override public void onResume() { super.onResume(); if (!isStaff()) viewModel.loadStores(); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Sets up the filter visibility toggle, considering user roles. + */ private void setupFilterToggle() { if (isStaff()) { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory); @@ -122,18 +146,30 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } } + /** + * Checks if the currently logged-in user has the STAFF role. + */ private boolean isStaff() { return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } + /** + * Sets up the search input listener. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true)); } + /** + * Configures the store filter spinner. + */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true)); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new InventoryAdapter(inventoryList, this); binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext())); @@ -156,10 +192,16 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn }); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true)); } + /** + * Loads inventory data based on current search query and store filter. + */ private void loadInventory(boolean reset) { String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : ""; if (query.isEmpty()) query = null; @@ -178,6 +220,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn viewModel.loadInventory(reset, query, storeId); } + /** + * Navigates to the inventory detail screen. + */ private void openDetail(InventoryDTO inv) { Bundle args = new Bundle(); if (inv != null) { @@ -186,6 +231,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args); } + /** + * Handles inventory item clicks by opening details. + */ @Override public void onInventoryClick(int position) { if (position >= 0 && position < inventoryList.size()) { @@ -193,6 +241,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } } + /** + * Forwards selection changes to the bulk delete handler. + */ @Override public void onSelectionChanged(int selectedCount) { if (bulkDeleteHandler != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 96225e6a..c7139548 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -33,6 +33,9 @@ import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and managing a list of pets. + */ @AndroidEntryPoint public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { private FragmentPetBinding binding; @@ -44,12 +47,18 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; + /** + * Initializes the view model. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(PetListViewModel.class); } + /** + * Inflates the layout and initializes UI components and filters. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -72,6 +81,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen return binding.getRoot(); } + /** + * Observes LiveData from the ViewModel to update the list and filter options. + */ private void observeViewModel() { viewModel.getPets().observe(getViewLifecycleOwner(), list -> { petList.clear(); @@ -94,6 +106,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen }); } + /** + * Configures the handler for bulk deletion of pets. + */ private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -107,6 +122,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen ); } + /** + * Refreshes pet data and filters when the fragment is resumed. + */ @Override public void onResume() { super.onResume(); @@ -115,6 +133,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen if (!isStaff()) viewModel.loadStores(); } + /** + * Sets up the visibility of filters based on the user's role. + */ private void setupFilterToggle() { if (isStaff()) { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet, @@ -126,32 +147,53 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } } + /** + * Checks if the current user has the 'STAFF' role. + */ private boolean isStaff() { return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } + /** + * Attaches search functionality to the search input field. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPet, () -> loadPetData(true)); } + /** + * Initializes the status filter spinner. + */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Available", "Adopted", "Owned", "Pending"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, () -> loadPetData(true)); } + /** + * Initializes the species filter spinner. + */ private void setupSpeciesFilter() { String[] initial = {"All Species"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, () -> loadPetData(true)); } + /** + * Initializes the store filter spinner. + */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadPetData(true)); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshPet.setOnRefreshListener(() -> loadPetData(true)); } + /** + * Triggers loading of pet data from the backend with current filters. + */ private void loadPetData(boolean reset) { String query = binding.etSearchPet.getText().toString().trim(); String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; @@ -171,6 +213,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen viewModel.loadPets(reset, query, status, species, storeId); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new PetAdapter(petList, this); adapter.setBaseUrl(baseUrl); @@ -195,6 +240,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen }); } + /** + * Navigates to the profile view of a specific pet. + */ private void openPetProfile(int position) { Bundle args = new Bundle(); PetDTO pet = petList.get(position); @@ -202,15 +250,24 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); } + /** + * Navigates to the screen for adding a new pet. + */ private void openPetDetails() { NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail); } + /** + * Handles clicks on individual pets in the list. + */ @Override public void onPetClick(int position) { openPetProfile(position); } + /** + * Notifies the bulk delete handler when item selection changes. + */ @Override public void onSelectionChanged(int selectedCount) { if (bulkDeleteHandler != null) { @@ -218,6 +275,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } } + /** + * Cleans up the binding when the view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 0820aca6..34d30e6a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -31,6 +31,9 @@ import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and managing a list of products. + */ @AndroidEntryPoint public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener { @@ -41,12 +44,18 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc @Inject @Named("baseUrl") String baseUrl; + /** + * Initializes the ViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ProductListViewModel.class); } + /** + * Inflates the layout and sets up UI components. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -66,6 +75,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc return binding.getRoot(); } + /** + * Observes the ViewModel for product list, categories, and loading status. + */ private void observeViewModel() { viewModel.getProducts().observe(getViewLifecycleOwner(), list -> { productList.clear(); @@ -83,6 +95,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc }); } + /** + * Reloads product data and categories when the fragment resumes. + */ @Override public void onResume() { super.onResume(); @@ -90,23 +105,38 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc viewModel.loadCategories(); } + /** + * Sets up the filter visibility toggle. + */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchProduct, binding.spinnerCategory); } + /** + * Sets up the search input listener. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchProduct, () -> loadProductData(true)); } + /** + * Configures the category filter spinner. + */ private void setupCategoryFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, () -> loadProductData(true)); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshProduct.setOnRefreshListener(() -> loadProductData(true)); } + /** + * Loads product data based on current filters and search query. + */ private void loadProductData(boolean reset) { String query = binding.etSearchProduct.getText().toString().trim(); if (query.isEmpty()) query = null; @@ -120,6 +150,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc viewModel.loadProducts(reset, query, categoryId); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new ProductAdapter(productList, this); adapter.setBaseUrl(baseUrl); @@ -143,6 +176,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc }); } + /** + * Navigates to the product detail screen. + */ private void openProductDetails(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -152,11 +188,17 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args); } + /** + * Handles product item clicks by opening details. + */ @Override public void onProductClick(int position) { openProductDetails(position); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index c4751ae3..d3b63523 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -29,6 +29,9 @@ import java.util.List; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and managing the relationships between products and suppliers. + */ @AndroidEntryPoint public class ProductSupplierFragment extends Fragment implements ProductSupplierAdapter.OnProductSupplierClickListener { @@ -40,12 +43,18 @@ public class ProductSupplierFragment extends Fragment private ProductSupplierListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; + /** + * Initializes the ViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ProductSupplierListViewModel.class); } + /** + * Inflates the layout and sets up UI components, filters, and observers. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -67,6 +76,9 @@ public class ProductSupplierFragment extends Fragment return binding.getRoot(); } + /** + * Observes the ViewModel for product-supplier list, products, suppliers, and loading status. + */ private void observeViewModel() { viewModel.getProductSuppliers().observe(getViewLifecycleOwner(), list -> { psList.clear(); @@ -89,6 +101,9 @@ public class ProductSupplierFragment extends Fragment }); } + /** + * Configures the bulk delete handler for multiple product-supplier relationship deletion. + */ private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -102,6 +117,9 @@ public class ProductSupplierFragment extends Fragment ); } + /** + * Reloads data and filter options when the fragment resumes. + */ @Override public void onResume() { super.onResume(); @@ -109,17 +127,26 @@ public class ProductSupplierFragment extends Fragment viewModel.loadFilterData(); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Sets up the filter visibility toggle. + */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS, binding.spinnerProduct, binding.spinnerSupplier); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new ProductSupplierAdapter(psList, this); binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext())); @@ -142,22 +169,37 @@ public class ProductSupplierFragment extends Fragment }); } + /** + * Sets up the search input listener. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPS, () -> loadData(true)); } + /** + * Configures the product filter spinner. + */ private void setupProductFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, () -> loadData(true)); } + /** + * Configures the supplier filter spinner. + */ private void setupSupplierFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, () -> loadData(true)); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshPS.setOnRefreshListener(() -> loadData(true)); } + /** + * Loads product-supplier data based on current search query and filters. + */ private void loadData(boolean reset) { String query = binding.etSearchPS.getText().toString().trim(); if (query.isEmpty()) query = null; @@ -177,6 +219,9 @@ public class ProductSupplierFragment extends Fragment viewModel.loadProductSuppliers(reset, query, productId, supplierId); } + /** + * Navigates to the product-supplier detail screen. + */ private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -187,9 +232,15 @@ public class ProductSupplierFragment extends Fragment NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args); } + /** + * Handles product-supplier item clicks by opening details. + */ @Override public void onProductSupplierClick(int position) { openDetail(position); } + /** + * Forwards selection changes to the bulk delete handler. + */ @Override public void onSelectionChanged(int count) { if (bulkDeleteHandler != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index f4e4e230..ea4a2bbd 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -30,6 +30,9 @@ import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and managing a list of purchase orders. + */ @AndroidEntryPoint public class PurchaseOrderFragment extends Fragment implements PurchaseOrderAdapter.OnPurchaseOrderClickListener { @@ -41,12 +44,18 @@ public class PurchaseOrderFragment extends Fragment @Inject TokenManager tokenManager; + /** + * Initializes the ViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(PurchaseOrderListViewModel.class); } + /** + * Inflates the layout and sets up UI components, filters, and observers. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -64,6 +73,9 @@ public class PurchaseOrderFragment extends Fragment return binding.getRoot(); } + /** + * Observes the ViewModel for purchase order list, stores, and loading status. + */ private void observeViewModel() { viewModel.getPurchaseOrders().observe(getViewLifecycleOwner(), list -> { poList.clear(); @@ -81,6 +93,9 @@ public class PurchaseOrderFragment extends Fragment }); } + /** + * Reloads data and stores if necessary when the fragment resumes. + */ @Override public void onResume() { super.onResume(); @@ -88,6 +103,9 @@ public class PurchaseOrderFragment extends Fragment if (!isStaff()) viewModel.loadStores(); } + /** + * Sets up the filter visibility toggle + */ private void setupFilterToggle() { if (isStaff()) { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPO); @@ -97,18 +115,30 @@ public class PurchaseOrderFragment extends Fragment } } + /** + * Checks if the currently logged-in user has the STAFF role. + */ private boolean isStaff() { return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } + /** + * Sets up the search input listener. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchPO, () -> loadData(true)); } + /** + * Configures the store filter spinner. + */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadData(true)); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new PurchaseOrderAdapter(poList, this); binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext())); @@ -131,10 +161,16 @@ public class PurchaseOrderFragment extends Fragment }); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshPO.setOnRefreshListener(() -> loadData(true)); } + /** + * Loads purchase order data based on current search query and store filter. + */ private void loadData(boolean reset) { String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : ""; if (query.isEmpty()) query = null; @@ -153,6 +189,9 @@ public class PurchaseOrderFragment extends Fragment viewModel.loadPurchaseOrders(reset, query, storeId); } + /** + * Navigates to the purchase order detail screen. + */ private void openDetail(int position) { Bundle args = new Bundle(); PurchaseOrderDTO po = poList.get(position); @@ -160,11 +199,17 @@ public class PurchaseOrderFragment extends Fragment NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args); } + /** + * Handles purchase order item clicks by opening details. + */ @Override public void onPurchaseOrderClick(int position) { openDetail(position); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index f6d0c388..890f8c68 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -31,6 +31,9 @@ import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and managing a list of sales. + */ @AndroidEntryPoint public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { @@ -41,6 +44,9 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis @Inject TokenManager tokenManager; + /** + * Inflates the layout for this fragment. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -48,6 +54,9 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis return binding.getRoot(); } + /** + * Initializes UI components and observers after the view is created. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -79,6 +88,9 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis NavHostFragment.findNavController(this).navigate(R.id.nav_refund)); } + /** + * Observes LiveData from the ViewModel to update the list and filter options. + */ private void observeViewModel() { viewModel.getSales().observe(getViewLifecycleOwner(), list -> { saleList.clear(); @@ -101,6 +113,9 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis }); } + /** + * Refreshes lookup data when the fragment is resumed. + */ @Override public void onResume() { super.onResume(); @@ -108,6 +123,9 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis viewModel.loadCustomers(); } + /** + * Sets up the visibility of filters. + */ private void setupFilterToggle() { if (isStaff()) { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale, @@ -119,32 +137,53 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis } } + /** + * Checks if the current user has the 'STAFF' role. + */ private boolean isStaff() { return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } + /** + * Checks if the current user has the 'ADMIN' role. + */ private boolean isAdmin() { return "ADMIN".equalsIgnoreCase(tokenManager.getRole()); } + /** + * Initializes the store filter spinner. + */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadSales(true)); } + /** + * Initializes the payment method filter spinner. + */ private void setupPaymentMethodFilter() { String[] paymentMethods = {"All Payments", "Cash", "Card"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerPaymentMethod, paymentMethods, () -> loadSales(true)); } + /** + * Initializes the refund status filter spinner. + */ private void setupRefundStatusFilter() { String[] refundStatuses = {"All Status", "Sale", "Refund"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerRefundStatus, refundStatuses, () -> loadSales(true)); } + /** + * Initializes the customer filter spinner. + */ private void setupCustomerFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerCustomer, () -> loadSales(true)); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new SaleAdapter(saleList, this); binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); @@ -168,14 +207,23 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis }); } + /** + * Attaches search functionality to the search input field. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchSale, () -> loadSales(true)); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshSale.setOnRefreshListener(() -> loadSales(true)); } + /** + * Triggers loading of sale data from the backend with current filters. + */ private void loadSales(boolean reset) { String query = binding.etSearchSale != null ? binding.etSearchSale.getText().toString().trim() : ""; if (query.isEmpty()) query = null; @@ -210,6 +258,9 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis viewModel.loadSales(reset, query, paymentMethod, storeId, isRefund, customerId); } + /** + * Handles clicks on individual sales in the list. + */ @Override public void onSaleClick(int position) { if (position < 0 || position >= saleList.size()) return; @@ -225,6 +276,9 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis NavHostFragment.findNavController(this).navigate(R.id.nav_sale_detail, args); } + /** + * Cleans up the binding when the view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index 1aaf625d..d5a73312 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -39,12 +39,18 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic private ServiceListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; + /** + * Initializes the ViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ServiceListViewModel.class); } + /** + * Inflates the layout and sets up UI components and observers. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -67,6 +73,9 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic return binding.getRoot(); } + /** + * Observes the ViewModel for service list updates and loading status. + */ private void observeViewModel() { viewModel.getServices().observe(getViewLifecycleOwner(), list -> { serviceList.clear(); @@ -79,6 +88,9 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic }); } + /** + * Configures the bulk delete handler for multiple service deletion. + */ private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -92,20 +104,32 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic ); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Sets up the filter visibility toggle. + */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchService); } + /** + * Sets up the search input listener. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchService, () -> loadServices(true)); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new ServiceAdapter(serviceList, this); binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext())); @@ -128,16 +152,25 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic }); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshService.setOnRefreshListener(() -> loadServices(true)); } + /** + * Loads service data based on current filters and search query. + */ private void loadServices(boolean reset) { String query = binding.etSearchService.getText().toString().trim(); if (query.isEmpty()) query = null; viewModel.loadServices(reset, query); } + /** + * Navigates to the service detail screen. + */ private void openDetail(ServiceDTO service) { Bundle args = new Bundle(); if (service != null) { @@ -146,6 +179,9 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail, args); } + /** + * Handles service item clicks by opening details. + */ @Override public void onServiceClick(int position) { if (position >= 0 && position < serviceList.size()) { @@ -153,6 +189,9 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic } } + /** + * Forwards selection changes to the bulk delete handler. + */ @Override public void onSelectionChanged(int count) { if (bulkDeleteHandler != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java index e1e92ca7..81efdd4a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/StaffFragment.java @@ -24,6 +24,9 @@ import java.util.*; import javax.inject.Inject; import javax.inject.Named; +/** + * Fragment for displaying and managing a list of staff members. + */ @AndroidEntryPoint public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmployeeClickListener { @@ -35,6 +38,9 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; + /** + * Inflates the layout and initializes UI components, filters, and observers. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -60,6 +66,9 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye return binding.getRoot(); } + /** + * Observes LiveData from the ViewModel to update the list and filter options. + */ private void observeViewModel() { viewModel.getFilteredEmployees().observe(getViewLifecycleOwner(), list -> { staffList.clear(); @@ -77,6 +86,9 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye }); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new EmployeeAdapter(staffList, this); adapter.setBaseUrl(baseUrl); @@ -101,19 +113,31 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye }); } + /** + * Initializes the status filter spinner. + */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Active", "Inactive"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusStaff, statuses, this::applyFilters); } + /** + * Initializes the store filter spinner. + */ private void setupStoreFilter() { SpinnerUtils.setupFilterSpinner(binding.spinnerStoreStaff, this::applyFilters); } + /** + * Attaches search functionality to the search input field. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchStaff, this::applyFilters); } + /** + * Applies the selected filters and triggers a data reload from the view model. + */ private void applyFilters() { String query = binding.etSearchStaff.getText().toString().trim(); String status = binding.spinnerStatusStaff.getSelectedItem() != null ? @@ -128,10 +152,16 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye viewModel.filter(query, storeId, status); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshStaff.setOnRefreshListener(() -> viewModel.loadStaff(true)); } + /** + * Navigates to the staff detail screen for adding or editing an employee. + */ private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -149,11 +179,17 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye NavHostFragment.findNavController(this).navigate(R.id.nav_staff_detail, args); } + /** + * Handles clicks on individual staff members in the list. + */ @Override public void onEmployeeClick(int position) { openDetail(position); } + /** + * Cleans up the binding when the view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index 163ac217..ca10c7d0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -27,6 +27,9 @@ import java.util.List; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and managing a list of suppliers. + */ @AndroidEntryPoint public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupplierClickListener { @@ -36,12 +39,18 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp private SupplierListViewModel viewModel; private BulkDeleteHandler bulkDeleteHandler; + /** + * Initializes the ViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(SupplierListViewModel.class); } + /** + * Inflates the layout and sets up UI components, bulk delete, and observers. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -63,6 +72,9 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp return binding.getRoot(); } + /** + * Observes the ViewModel for supplier list updates and loading status. + */ private void observeViewModel() { viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> { supplierList.clear(); @@ -75,6 +87,9 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp }); } + /** + * Configures the bulk delete handler for multiple supplier deletion. + */ private void setupBulkDelete() { bulkDeleteHandler = new BulkDeleteHandler( this, @@ -88,24 +103,39 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp ); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Sets up the filter visibility toggle. + */ private void setupFilterToggle() { UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSupplier); } + /** + * Sets up the search input listener. + */ private void setupSearch() { UIUtils.attachSearch(binding.etSearchSupplier, () -> loadSupplierData(true)); } + /** + * Configures the swipe-to-refresh layout. + */ private void setupSwipeRefresh() { binding.swipeRefreshSupplier.setOnRefreshListener(() -> loadSupplierData(true)); } + /** + * Navigates to the supplier detail screen. + */ private void openSupplierDetails(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -115,11 +145,17 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args); } + /** + * Handles supplier item clicks by opening details. + */ @Override public void onSupplierClick(int position) { openSupplierDetails(position); } + /** + * Forwards selection changes to the bulk delete handler. + */ @Override public void onSelectionChanged(int count) { if (bulkDeleteHandler != null) { @@ -127,12 +163,18 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp } } + /** + * Loads supplier data based on current search query. + */ private void loadSupplierData(boolean reset) { String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : ""; if (query.isEmpty()) query = null; viewModel.loadSuppliers(reset, query); } + /** + * Configures the RecyclerView and its scroll listener for pagination. + */ private void setupRecyclerView() { adapter = new SupplierAdapter(supplierList, this); binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext())); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 1cd47561..64ec51d8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -39,12 +39,18 @@ public class AdoptionDetailFragment extends Fragment { @Inject TokenManager tokenManager; + /** + * Initializes the fragment and its ViewModel. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(AdoptionDetailViewModel.class); } + /** + * Inflates the layout for the fragment + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -52,6 +58,9 @@ public class AdoptionDetailFragment extends Fragment { return binding.getRoot(); } + /** + * Sets up UI components, observers, and loads initial data after the view is created. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -68,6 +77,9 @@ public class AdoptionDetailFragment extends Fragment { binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete()); } + /** + * Sets up observers for ViewModel to update the UI dynamically. + */ private void observeViewModel() { viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); @@ -113,18 +125,27 @@ public class AdoptionDetailFragment extends Fragment { }); } + /** + * Shows or hides the loading bar. + */ private void setLoading(boolean loading) { if (binding != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Initializes the spinners with data and selection listeners. + */ private void setupSpinners() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, new String[]{}); @@ -141,11 +162,17 @@ public class AdoptionDetailFragment extends Fragment { SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAdoptionStatus, p -> notifyDateStatusChange()); } + /** + * Configures the date picker for the adoption date field. + */ private void setupDatePicker() { binding.etAdoptionDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAdoptionDate, this::notifyDateStatusChange)); } + /** + * Notifies the ViewModel when the date or status changes to update available options. + */ private void notifyDateStatusChange() { if (isUpdatingUI) return; String date = binding.etAdoptionDate.getText().toString(); @@ -154,6 +181,9 @@ public class AdoptionDetailFragment extends Fragment { viewModel.onDateChanged(date, status); } + /** + * Uses fragment arguments to determine if we are editing an existing record. + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("adoptionId")) { @@ -164,6 +194,9 @@ public class AdoptionDetailFragment extends Fragment { viewModel.setAdoptionId(-1); } + /** + * Load the adoption data from the backend. + */ private void loadAdoptionData() { viewModel.loadAdoption().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -174,6 +207,10 @@ public class AdoptionDetailFragment extends Fragment { }); } + /** + * Applies the current ViewState to the UI elements. + * This handles enabling/disabling views and setting text/selections based on state. + */ private void applyViewState(AdoptionDetailViewModel.ViewState state) { isUpdatingUI = true; @@ -224,10 +261,16 @@ public class AdoptionDetailFragment extends Fragment { isUpdatingUI = false; } + /** + * Checks if the currently logged-in user has the STAFF role. + */ private boolean isStaff() { return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } + /** + * Validates input and saves the adoption record. + */ private void saveAdoption() { if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionCustomer, "Customer")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerAdoptionPet, "Pet")) return; @@ -275,6 +318,9 @@ public class AdoptionDetailFragment extends Fragment { }); } + /** + * Displays a confirmation dialog before deleting the adoption record. + */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption Record", () -> viewModel.deleteAdoption().observe(getViewLifecycleOwner(), resource -> { @@ -289,6 +335,9 @@ public class AdoptionDetailFragment extends Fragment { })); } + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index baec0c28..a426cf8e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -47,18 +47,27 @@ public class AppointmentDetailFragment extends Fragment { @Inject TokenManager tokenManager; + /** + * Initializes the fragment and its ViewModel. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(AppointmentDetailViewModel.class); } + /** + * Inflates the layout for the fragment + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false); return binding.getRoot(); } + /** + * Sets up UI components, observers, and loads initial data after the view is created. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -73,12 +82,18 @@ public class AppointmentDetailFragment extends Fragment { binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete()); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Initializes the spinners with data and selection listeners. + */ private void setupSpinners() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, new String[]{}); @@ -108,11 +123,17 @@ public class AppointmentDetailFragment extends Fragment { SpinnerUtils.setOnIndexSelectedListener(binding.spinnerAppointmentStatus, p -> notifyDateTimeStatusChange()); } + /** + * Configures the date picker for the appointment date field. + */ private void setupDatePicker() { binding.etAppointmentDate.setOnClickListener(v -> UIUtils.showDatePicker(requireContext(), binding.etAppointmentDate, this::notifyDateTimeStatusChange)); } + /** + * Sets up observers for ViewModel to update the UI dynamically. + */ private void observeViewModel() { viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); @@ -147,12 +168,19 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Shows or hides the loading bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Applies the current ViewState to the UI elements. + * This handles enabling/disabling views and setting text/selections based on state. + */ private void applyViewState(AppointmentDetailViewModel.ViewState state) { isUpdatingUI = true; @@ -201,10 +229,16 @@ public class AppointmentDetailFragment extends Fragment { isUpdatingUI = false; } + /** + * Checks if the currently logged-in user has the STAFF role. + */ private boolean isStaff() { return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } + /** + * Notifies the ViewModel when the date, time, or status changes to update available options. + */ private void notifyDateTimeStatusChange() { if (isUpdatingUI) return; @@ -215,6 +249,9 @@ public class AppointmentDetailFragment extends Fragment { viewModel.onDateOrTimeChanged(date, time, status); } + /** + * Uses fragment arguments to determine if we are editing an existing record. + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("appointmentId")) { @@ -225,6 +262,9 @@ public class AppointmentDetailFragment extends Fragment { } } + /** + * Load the appointment data from the backend. + */ private void loadAppointmentData() { viewModel.loadAppointment().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -244,6 +284,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Validates input and saves the appointment record. + */ private void saveAppointment() { if (!validateRequiredFields()) return; @@ -275,6 +318,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Validates that all required fields are selected or filled. + */ private boolean validateRequiredFields() { if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Customer")) return false; if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return false; @@ -284,15 +330,24 @@ public class AppointmentDetailFragment extends Fragment { return true; } + /** + * Formats the selected hour and minute from spinners into a time string. + */ private String buildTimeString() { return DateTimeUtils.formatTime(HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); } + /** + * Handles errors that occur during the save process. + */ private void handleSaveError(String errorMessage) { if (errorMessage != null && errorMessage.toLowerCase().contains("not available")) showNoAvailabilityDialog(); else Toast.makeText(getContext(), errorMessage != null ? errorMessage : "Error saving", Toast.LENGTH_SHORT).show(); } + /** + * Displays a dialog when the selected time slot is not available. + */ private void showNoAvailabilityDialog() { new androidx.appcompat.app.AlertDialog.Builder(requireContext()) .setTitle("No Availability") @@ -301,6 +356,9 @@ public class AppointmentDetailFragment extends Fragment { .setNegativeButton("Cancel Booking", (d, w) -> navigateBack()).show(); } + /** + * Displays a confirmation dialog before deleting the appointment record. + */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () -> viewModel.deleteAppointment().observe(getViewLifecycleOwner(), resource -> { @@ -310,10 +368,16 @@ public class AppointmentDetailFragment extends Fragment { })); } + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + /** + * Parses a time string and sets the hour and minute spinners accordingly. + */ private void parseAndSetTimeSpinners(String time) { int[] parsedTime = DateTimeUtils.parseTimeString(time); if (parsedTime == null) return; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java index 0d243a69..47145bcf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CouponDetailFragment.java @@ -29,12 +29,18 @@ import java.util.Locale; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and editing coupon details. + */ @AndroidEntryPoint public class CouponDetailFragment extends Fragment { private FragmentCouponDetailBinding binding; private CouponDetailViewModel viewModel; private long couponId = -1; + /** + * Inflates the layout, initializes ViewModel, and sets up UI components. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -74,10 +80,16 @@ public class CouponDetailFragment extends Fragment { return binding.getRoot(); } + /** + * Initializes the discount type spinner with options. + */ private void setupDiscountTypeSpinner() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerDiscountTypeDetail, new String[]{"FIXED", "PERCENT"}); } + /** + * Configures a date picker for an EditText field. + */ private void setupDatePicker(android.widget.EditText editText, android.widget.EditText dependOn, Runnable onDateSet) { editText.setFocusable(false); editText.setClickable(true); @@ -91,6 +103,9 @@ public class CouponDetailFragment extends Fragment { }); } + /** + * Loads coupon details from the backend and populates the UI. + */ private void loadCouponDetails() { binding.tvCouponId.setText(DateTimeUtils.formatId(couponId)); binding.tvCouponId.setVisibility(View.VISIBLE); @@ -111,6 +126,9 @@ public class CouponDetailFragment extends Fragment { }); } + /** + * Validates input and saves the coupon record. + */ private void saveCoupon() { if (!InputValidator.isNotEmpty(binding.etCouponCodeDetail, "Coupon Code")) return; if (!InputValidator.isGreaterThanZero(binding.etDiscountValueDetail, "Discount Value")) return; @@ -156,6 +174,9 @@ public class CouponDetailFragment extends Fragment { }); } + /** + * Displays a confirmation dialog before deleting the coupon. + */ private void confirmDelete() { new AlertDialog.Builder(requireContext()) .setTitle("Delete Coupon") diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java index 455450cc..017c80e2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/CustomerDetailFragment.java @@ -21,6 +21,9 @@ import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and editing customer details. + */ @AndroidEntryPoint public class CustomerDetailFragment extends Fragment { @@ -31,6 +34,9 @@ public class CustomerDetailFragment extends Fragment { private final String[] STATUSES = {"Active", "Inactive"}; + /** + * Inflates the layout, initializes ViewModel, and sets up UI components. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -49,10 +55,16 @@ public class CustomerDetailFragment extends Fragment { return binding.getRoot(); } + /** + * Initializes the spinners with data. + */ private void setupSpinners() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerCustomerStatus, STATUSES); } + /** + * Uses fragment arguments to determine if we are editing an existing record. + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.getBoolean("isEditing", false)) { @@ -86,6 +98,9 @@ public class CustomerDetailFragment extends Fragment { } } + /** + * Loads customer data from the backend. + */ private void loadCustomerData(long id) { viewModel.loadCustomer(id).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { @@ -105,12 +120,18 @@ public class CustomerDetailFragment extends Fragment { }); } + /** + * Shows or hides the loading bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Validates input and saves the customer record. + */ private void save() { if (!InputValidator.isNotEmpty(binding.etCustomerUsername, "Username")) return; @@ -160,6 +181,9 @@ public class CustomerDetailFragment extends Fragment { }); } + /** + * Displays a confirmation dialog before deleting the customer. + */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Customer", () -> viewModel.deleteCustomer().observe(getViewLifecycleOwner(), resource -> { @@ -175,10 +199,16 @@ public class CustomerDetailFragment extends Fragment { })); } + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index 75f1a6b3..f0e37927 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -37,12 +37,18 @@ public class InventoryDetailFragment extends Fragment { private long preselectedStoreId = -1; private long preselectedProductId = -1; + /** + * Initializes the fragment and its ViewModel. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(InventoryDetailViewModel.class); } + /** + * Inflates the layout for the fragment. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -50,6 +56,9 @@ public class InventoryDetailFragment extends Fragment { return binding.getRoot(); } + /** + * Sets up UI components, observers, and loads initial data after the view is created. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -63,23 +72,35 @@ public class InventoryDetailFragment extends Fragment { binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete()); } + /** + * Sets up observers for ViewModel to update the UI dynamically. + */ private void observeViewModel() { viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner()); viewModel.getProductList().observe(getViewLifecycleOwner(), list -> refreshProductSpinner()); } + /** + * Shows or hides the loading bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Loads initial data for the store and product spinners. + */ private void loadSpinnersData() { viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -97,18 +118,27 @@ public class InventoryDetailFragment extends Fragment { }); } + /** + * Refreshes the store spinner with the current data. + */ private void refreshStoreSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, viewModel.getStoreList().getValue(), DropdownDTO::getLabel, "-- Select Store --", preselectedStoreId, DropdownDTO::getId); } + /** + * Refreshes the product spinner with the current data. + */ private void refreshProductSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, viewModel.getProductList().getValue(), DropdownDTO::getLabel, "-- Select Product --", preselectedProductId, DropdownDTO::getId); } + /** + * Uses fragment arguments to determine if we are editing an existing record. + */ private void handleArguments() { Bundle args = getArguments(); if (args != null && args.containsKey("inventoryId")) { @@ -131,6 +161,9 @@ public class InventoryDetailFragment extends Fragment { } } + /** + * Loads the inventory data from the backend. + */ private void loadInventoryData() { viewModel.loadInventory().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -149,6 +182,9 @@ public class InventoryDetailFragment extends Fragment { }); } + /** + * Validates input and saves the inventory record. + */ private void saveInventory() { if (!InputValidator.isSpinnerSelected(binding.spinnerInventoryStore, "Store")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerInventoryProduct, "Product")) return; @@ -176,10 +212,16 @@ public class InventoryDetailFragment extends Fragment { }); } + /** + * Displays a confirmation dialog before deleting the inventory record. + */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Inventory Item", this::deleteInventory); } + /** + * Calls the ViewModel to delete the inventory record. + */ private void deleteInventory() { setButtonsEnabled(false); viewModel.deleteInventory().observe(getViewLifecycleOwner(), resource -> { @@ -197,10 +239,16 @@ public class InventoryDetailFragment extends Fragment { }); } + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + /** + * Enables or disables the action buttons. + */ private void setButtonsEnabled(boolean enabled) { UIUtils.setViewsEnabled(enabled, binding.btnSaveInventory, binding.btnDeleteInventory, binding.btnInventoryBack); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 5d89e305..7119a447 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -58,6 +58,9 @@ public class ProductDetailFragment extends Fragment { @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; + /** + * Initializes the fragment, its ViewModel, and the ImagePickerHelper. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -88,6 +91,9 @@ public class ProductDetailFragment extends Fragment { }); } + /** + * Inflates the layout for the fragment. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -95,6 +101,9 @@ public class ProductDetailFragment extends Fragment { return binding.getRoot(); } + /** + * Sets up UI components, observers, and handles arguments after the view is created. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -108,6 +117,9 @@ public class ProductDetailFragment extends Fragment { binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage)); } + /** + * Sets up observers for the ViewModel and loads product categories. + */ private void observeViewModel() { viewModel.getCategoryList().observe(getViewLifecycleOwner(), list -> updateCategorySpinner()); @@ -120,24 +132,36 @@ public class ProductDetailFragment extends Fragment { }); } + /** + * Shows or hides the loading bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Populates the category spinner with available categories. + */ private void updateCategorySpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, viewModel.getCategoryList().getValue(), DropdownDTO::getLabel, "-- Select Category --", preselectedCategoryId, DropdownDTO::getId); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Uses fragment arguments to determine if we are editing an existing record. + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("prodId")) { @@ -158,6 +182,9 @@ public class ProductDetailFragment extends Fragment { } } + /** + * Loads the product details from the backend. + */ private void loadProductData() { viewModel.loadProduct().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -175,6 +202,9 @@ public class ProductDetailFragment extends Fragment { }); } + /** + * Loads the product image from the server. + */ private void loadProductImage() { String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, viewModel.getProdId()); String token = tokenManager.getToken(); @@ -192,6 +222,9 @@ public class ProductDetailFragment extends Fragment { }); } + /** + * Performs image-related actions (removal or upload) after a successful product save. + */ private void performPendingImageActions(String successMsg) { if (isImageRemoved) { viewModel.deleteProductImage().observe(getViewLifecycleOwner(), resource -> { @@ -214,6 +247,9 @@ public class ProductDetailFragment extends Fragment { } } + /** + * Uploads the selected product image to the server. + */ private void uploadProductImageAndNavigate(Uri uri, String successMsg) { File file = FileUtils.getFileFromUri(requireContext(), uri); if (file == null) { @@ -239,6 +275,9 @@ public class ProductDetailFragment extends Fragment { }); } + /** + * Validates input and saves the product record. + */ private void saveProduct() { if (!InputValidator.isNotEmpty(binding.etProductName, "Product Name")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerProductCategory, "Category")) return; @@ -267,6 +306,9 @@ public class ProductDetailFragment extends Fragment { }); } + /** + * Displays a confirmation dialog before deleting the product. + */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () -> viewModel.deleteProduct().observe(getViewLifecycleOwner(), resource -> { @@ -280,6 +322,9 @@ public class ProductDetailFragment extends Fragment { })); } + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index 6abb918c..e0ba9409 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -35,12 +35,18 @@ public class ProductSupplierDetailFragment extends Fragment { private long preselectedProductId = -1; private long preselectedSupplierId = -1; + /** + * Initializes the view model. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ProductSupplierDetailViewModel.class); } + /** + * Inflates the layout for this fragment. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -48,6 +54,9 @@ public class ProductSupplierDetailFragment extends Fragment { return binding.getRoot(); } + /** + * Initializes the UI components and observers after the view is created. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -60,23 +69,35 @@ public class ProductSupplierDetailFragment extends Fragment { binding.btnDeletePS.setOnClickListener(v -> confirmDelete()); } + /** + * Observes LiveData from the ViewModel to update the UI. + */ private void observeViewModel() { viewModel.getProductList().observe(getViewLifecycleOwner(), list -> refreshProductSpinner()); viewModel.getSupplierList().observe(getViewLifecycleOwner(), list -> refreshSupplierSpinner()); } + /** + * Toggles the visibility of the progress bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Cleans up the binding when the view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Loads lookup data for products and suppliers. + */ private void loadSpinnersData() { viewModel.loadProducts().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -94,18 +115,27 @@ public class ProductSupplierDetailFragment extends Fragment { }); } + /** + * Refreshes the product spinner with data from the view model. + */ private void refreshProductSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, viewModel.getProductList().getValue(), ProductDTO::getProdName, "-- Select Product --", preselectedProductId, ProductDTO::getProdId); } + /** + * Refreshes the supplier spinner with data from the view model. + */ private void refreshSupplierSpinner() { SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, viewModel.getSupplierList().getValue(), SupplierDTO::getSupCompany, "-- Select Supplier --", preselectedSupplierId, SupplierDTO::getSupId); } + /** + * Handles fragment arguments to determine mode (new vs edit). + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("productId") && a.containsKey("supplierId")) { @@ -132,6 +162,9 @@ public class ProductSupplierDetailFragment extends Fragment { } } + /** + * Validates and saves the product-supplier relationship. + */ private void save() { if (!InputValidator.isSpinnerSelected(binding.spinnerPSProduct, "Product")) return; if (!InputValidator.isSpinnerSelected(binding.spinnerPSSupplier, "Supplier")) return; @@ -155,6 +188,9 @@ public class ProductSupplierDetailFragment extends Fragment { }); } + /** + * Shows a confirmation dialog before deleting the relationship. + */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier Relationship", () -> viewModel.deleteProductSupplier().observe(getViewLifecycleOwner(), resource -> { @@ -169,6 +205,9 @@ public class ProductSupplierDetailFragment extends Fragment { })); } + /** + * Navigates back to the previous fragment. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java index d1bbd31c..aca42375 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java @@ -27,6 +27,9 @@ public class PurchaseOrderDetailFragment extends Fragment { private PurchaseOrderDetailViewModel viewModel; private long purchaseOrderId; + /** + * Initializes the view model. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -57,6 +60,9 @@ public class PurchaseOrderDetailFragment extends Fragment { }); } + /** + * Handles fragment arguments to retrieve the purchase order ID. + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("purchaseOrderId")) { @@ -65,12 +71,18 @@ public class PurchaseOrderDetailFragment extends Fragment { } } + /** + * Toggles the visibility of the progress bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Loads the details of the specified purchase order. + */ private void loadPurchaseOrderData() { viewModel.loadPurchaseOrder(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -87,6 +99,9 @@ public class PurchaseOrderDetailFragment extends Fragment { }); } + /** + * Cleans up the binding when the view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java index 0dbd985f..6165be7b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java @@ -21,6 +21,9 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; +/** + * Fragment for processing refunds for existing sales. + */ @AndroidEntryPoint public class RefundFragment extends Fragment { @@ -29,6 +32,9 @@ public class RefundFragment extends Fragment { private final String[] PAYMENT_METHODS = {"Cash", "Card"}; + /** + * Initializes the fragment's UI and view model. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -51,25 +57,37 @@ public class RefundFragment extends Fragment { return binding.getRoot(); } + /** + * Sets up the payment method spinner. + */ private void setupSpinner() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerRefundPayment, PAYMENT_METHODS); } + /** + * Observes LiveData from the ViewModel to update the UI. + */ private void observeViewModel() { viewModel.getAvailableItems().observe(getViewLifecycleOwner(), items -> renderOriginalItems()); viewModel.getRefundCart().observe(getViewLifecycleOwner(), cart -> { renderRefundCart(); updateRefundTotal(); - renderOriginalItems(); // Re-render to reflect quantities in cart + renderOriginalItems(); }); } + /** + * Toggles the visibility of the progress bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Loads the list of all sales to allow looking up the sale to refund. + */ private void loadAllSales() { viewModel.loadAllSales().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -84,6 +102,9 @@ public class RefundFragment extends Fragment { }); } + /** + * Loads the details of a specific sale for refund processing. + */ private void loadSale() { String idStr = binding.etRefundSaleId.getText().toString().trim(); if (idStr.isEmpty()) { @@ -145,6 +166,9 @@ public class RefundFragment extends Fragment { binding.btnProcessRefund.setVisibility(View.VISIBLE); } + /** + * Renders the items from the original sale that are available for refund. + */ private void renderOriginalItems() { binding.llOriginalItems.removeAllViews(); List available = viewModel.getAvailableItems().getValue(); @@ -173,6 +197,9 @@ public class RefundFragment extends Fragment { } } + /** + * Renders the items selected for the refund. + */ private void renderRefundCart() { binding.llRefundItems.removeAllViews(); List cart = viewModel.getRefundCart().getValue(); @@ -200,6 +227,9 @@ public class RefundFragment extends Fragment { } } + /** + * Adds a header row to the layout representing a table. + */ private void addTableHeader(LinearLayout parent) { if (getContext() == null) return; LinearLayout header = new LinearLayout(getContext()); @@ -220,6 +250,9 @@ public class RefundFragment extends Fragment { parent.addView(header); } + /** + * Builds a UI row for an item in the original list or refund cart. + */ private LinearLayout buildItemRow(String name, int qty, BigDecimal unitPrice, boolean isAdd, Runnable action) { if (getContext() == null) return new LinearLayout(getContext()); @@ -267,6 +300,9 @@ public class RefundFragment extends Fragment { return row; } + /** + * Shows a dialog to select the quantity of an item to refund. + */ private void showQuantityDialog(RefundViewModel.RefundItem item, int available) { EditText input = new EditText(getContext()); input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); @@ -301,11 +337,17 @@ public class RefundFragment extends Fragment { .show(); } + /** + * Updates the total refund amount display. + */ private void updateRefundTotal() { BigDecimal total = viewModel.calculateRefundTotal(); binding.tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP)); } + /** + * Validates and prepares the refund for processing. + */ private void processRefund() { if (viewModel.getCurrentSale() == null) { Toast.makeText(getContext(), "Load a sale first", Toast.LENGTH_SHORT).show(); @@ -325,6 +367,9 @@ public class RefundFragment extends Fragment { () -> submitRefund(payment)); } + /** + * Submits the refund request to the backend. + */ private void submitRefund(String payment) { viewModel.submitRefund(payment).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { @@ -339,10 +384,16 @@ public class RefundFragment extends Fragment { }); } + /** + * Navigates back to the previous fragment. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + /** + * Cleans up the binding when the view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java index 4963b272..b1ecb2b9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -26,6 +26,9 @@ import dagger.hilt.android.AndroidEntryPoint; import java.math.BigDecimal; import java.util.*; +/** + * Fragment for viewing or creating sale details. + */ @AndroidEntryPoint public class SaleDetailFragment extends Fragment { @@ -36,6 +39,9 @@ public class SaleDetailFragment extends Fragment { private final String[] PAYMENT_METHODS = { "Cash", "Card"}; + /** + * Initializes the fragment's UI and view model. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -65,14 +71,23 @@ public class SaleDetailFragment extends Fragment { return binding.getRoot(); } + /** + * Checks if the current user has the 'STAFF' role. + */ private boolean isStaff() { return "STAFF".equalsIgnoreCase(tokenManager.getRole()); } + /** + * Checks if the current user has the 'ADMIN' role. + */ private boolean isAdmin() { return "ADMIN".equalsIgnoreCase(tokenManager.getRole()); } + /** + * Observes LiveData from the ViewModel to update the UI. + */ private void observeViewModel() { viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> { Long primaryStoreId = tokenManager.getPrimaryStoreId(); @@ -115,6 +130,9 @@ public class SaleDetailFragment extends Fragment { }); } + /** + * Handles fragment arguments to determine mode (new sale vs view existing). + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("saleId")) { @@ -159,12 +177,18 @@ public class SaleDetailFragment extends Fragment { } } + /** + * Toggles the visibility of the progress bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Loads lookup data for stores, customers, and products. + */ private void loadData() { viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -183,6 +207,9 @@ public class SaleDetailFragment extends Fragment { }); } + /** + * Loads the details of an existing sale. + */ private void loadSaleDetails() { viewModel.loadSaleDetails().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -241,6 +268,9 @@ public class SaleDetailFragment extends Fragment { }); } + /** + * Sets up the coupon application logic. + */ private void setupCoupon() { binding.btnApplyCoupon.setOnClickListener(v -> { String code = binding.etCouponCode.getText().toString().trim(); @@ -284,6 +314,9 @@ public class SaleDetailFragment extends Fragment { }); } + /** + * Updates the UI to reflect an applied coupon. + */ private void applyAppliedCouponUI(CouponDTO coupon) { String info; if ("PERCENTAGE".equalsIgnoreCase(coupon.getDiscountType())) { @@ -299,12 +332,18 @@ public class SaleDetailFragment extends Fragment { binding.etCouponCode.setEnabled(false); } + /** + * Displays an error message related to coupon validation. + */ private void showCouponError(String message) { binding.tvCouponInfo.setText(message); binding.tvCouponInfo.setTextColor(0xFFE53935); binding.tvCouponInfo.setVisibility(View.VISIBLE); } + /** + * Sets up the logic for adding items to the sale cart. + */ private void setupAddItem() { binding.btnAddItem.setOnClickListener(v -> { if (!InputValidator.isSpinnerSelected(binding.spinnerSaleProduct, "Product")) return; @@ -338,6 +377,9 @@ public class SaleDetailFragment extends Fragment { }); } + /** + * Renders the list of items currently in the cart. + */ private void renderCartItems() { binding.llSaleItems.removeAllViews(); List items = viewModel.getCartItems().getValue(); @@ -358,6 +400,9 @@ public class SaleDetailFragment extends Fragment { } } + /** + * Adds a row representing a sale item to the layout. + */ private void addItemRow(String name, int qty, BigDecimal price, Long prodId) { if (getContext() == null) return; LinearLayout row = new LinearLayout(getContext()); @@ -397,6 +442,9 @@ public class SaleDetailFragment extends Fragment { binding.llSaleItems.addView(row); } + /** + * Updates the subtotal, discounts, and total amount display. + */ private void updateTotal() { BigDecimal subtotal = viewModel.calculateSubtotal(); BigDecimal couponDiscount = viewModel.calculateCouponDiscount(); @@ -432,6 +480,9 @@ public class SaleDetailFragment extends Fragment { } } + /** + * Validates and saves the new sale. + */ private void saveSale() { if (!InputValidator.isSpinnerSelected(binding.spinnerSaleStore, "Store")) return; @@ -468,6 +519,9 @@ public class SaleDetailFragment extends Fragment { }); } + /** + * Shows a confirmation dialog before proceeding to the refund screen. + */ private void showRefundDialog() { DialogUtils.showConfirmDialog(requireContext(), "Process Refund", "Are you sure you want to process a refund for this sale?", () -> { @@ -477,10 +531,16 @@ public class SaleDetailFragment extends Fragment { }); } + /** + * Navigates back to the previous fragment. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + /** + * Cleans up the binding when the view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index d7ee6e4c..0a0612c5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -35,12 +35,18 @@ public class ServiceDetailFragment extends Fragment { private FragmentServiceDetailBinding binding; private ServiceDetailViewModel viewModel; + /** + * Initializes the fragment and its ViewModel. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ServiceDetailViewModel.class); } + /** + * Inflates the layout for the fragment. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -48,6 +54,9 @@ public class ServiceDetailFragment extends Fragment { return binding.getRoot(); } + /** + * Sets up UI components, observers, and handles arguments after the view is created. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -60,22 +69,34 @@ public class ServiceDetailFragment extends Fragment { binding.btnDeleteService.setOnClickListener(v -> deleteService()); } + /** + * Sets up observers for ViewModel to update the UI dynamically. + */ private void observeViewModel() { viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); } + /** + * Shows or hides the loading bar. + */ private void setLoading(boolean loading) { if (binding != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Validates input and saves the service record. + */ private void saveService() { if (!InputValidator.isNotEmpty(binding.etServiceName, "Service Name")) return; if (!InputValidator.isNotEmpty(binding.etServiceDesc, "Description")) return; @@ -111,6 +132,9 @@ public class ServiceDetailFragment extends Fragment { }); } + /** + * Displays a confirmation dialog before deleting the service record. + */ private void deleteService() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () -> viewModel.deleteService().observe(getViewLifecycleOwner(), resource -> { @@ -126,10 +150,16 @@ public class ServiceDetailFragment extends Fragment { })); } + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + /** + * Uses fragment arguments to determine if we are editing an existing record. + */ private void handleArguments() { if (getArguments() != null && getArguments().containsKey("serviceId")) { viewModel.setServiceId(getArguments().getLong("serviceId")); @@ -140,6 +170,9 @@ public class ServiceDetailFragment extends Fragment { viewModel.setServiceId(-1); } + /** + * Loads the service details from the backend. + */ private void loadServiceData() { viewModel.loadService().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -150,6 +183,9 @@ public class ServiceDetailFragment extends Fragment { }); } + /** + * Applies the current ViewState to the UI elements. + */ private void applyViewState(ServiceDetailViewModel.ViewState state) { binding.tvMode.setText(state.modeTitle); binding.tvServiceId.setText(DateTimeUtils.formatId(viewModel.getServiceId())); @@ -169,6 +205,9 @@ public class ServiceDetailFragment extends Fragment { updateIfDifferent(binding.etServicePrice, state.servicePrice); } + /** + * Updates an EditText field only if the new value is different from the current one. + */ private void updateIfDifferent(EditText field, String value) { String current = field.getText() != null ? field.getText().toString() : ""; String next = value != null ? value : ""; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java index 9bb3435d..0f69f7b5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/StaffDetailFragment.java @@ -22,6 +22,9 @@ import java.util.List; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying and editing staff details. + */ @AndroidEntryPoint public class StaffDetailFragment extends Fragment { @@ -32,6 +35,9 @@ public class StaffDetailFragment extends Fragment { private long preselectedStoreId = -1; + /** + * Inflates the layout, initializes ViewModel, and sets up UI components. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -52,14 +58,23 @@ public class StaffDetailFragment extends Fragment { return binding.getRoot(); } + /** + * Sets up observers for the ViewModel to refresh the store spinner. + */ private void observeViewModel() { viewModel.getStoreList().observe(getViewLifecycleOwner(), list -> refreshStoreSpinner()); } + /** + * Initializes the status spinner with options. + */ private void setupSpinners() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES); } + /** + * Loads the list of stores from the backend. + */ private void loadStores() { viewModel.loadStores().observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -68,6 +83,9 @@ public class StaffDetailFragment extends Fragment { }); } + /** + * Populates the store spinner with available stores. + */ private void refreshStoreSpinner() { List list = viewModel.getStoreList().getValue(); if (list == null) return; @@ -76,6 +94,9 @@ public class StaffDetailFragment extends Fragment { preselectedStoreId, DropdownDTO::getId); } + /** + * Uses fragment arguments to determine if we are editing an existing record. + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.getBoolean("isEditing", false)) { @@ -96,6 +117,9 @@ public class StaffDetailFragment extends Fragment { } } + /** + * Loads staff member details from the backend. + */ private void loadEmployeeData(long id) { viewModel.loadEmployee(id).observe(getViewLifecycleOwner(), resource -> { if (resource != null) { @@ -117,12 +141,18 @@ public class StaffDetailFragment extends Fragment { }); } + /** + * Shows or hides the loading bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Validates input and saves the staff record. + */ private void save() { if (!InputValidator.isNotEmpty(binding.etStaffUsername, "Username")) return; @@ -198,6 +228,9 @@ public class StaffDetailFragment extends Fragment { }); } + /** + * Displays a confirmation dialog before deleting the staff account. + */ private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Staff Account", () -> viewModel.deleteEmployee().observe(getViewLifecycleOwner(), resource -> { @@ -213,10 +246,16 @@ public class StaffDetailFragment extends Fragment { })); } + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index 5eb1f43b..513e627c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -35,12 +35,18 @@ public class SupplierDetailFragment extends Fragment { private FragmentSupplierDetailBinding binding; private SupplierDetailViewModel viewModel; + /** + * Initializes the view model. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(SupplierDetailViewModel.class); } + /** + * Inflates the layout for this fragment. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -48,6 +54,9 @@ public class SupplierDetailFragment extends Fragment { return binding.getRoot(); } + /** + * Initializes the UI components and observers after the view is created. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -61,22 +70,34 @@ public class SupplierDetailFragment extends Fragment { binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); } + /** + * Observes LiveData from the ViewModel to update the UI. + */ private void observeViewModel() { viewModel.getViewState().observe(getViewLifecycleOwner(), this::applyViewState); } + /** + * Toggles the visibility of the progress bar. + */ private void setLoading(boolean loading) { if (binding != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Cleans up the binding when the view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Validates and saves the supplier information. + */ private void saveSupplier() { if (!InputValidator.isNotEmpty(binding.etSupCompany, "Company Name")) return; if (!InputValidator.isNotEmpty(binding.etSupContactFirstName, "First Name")) return; @@ -115,6 +136,9 @@ public class SupplierDetailFragment extends Fragment { }); } + /** + * Deletes the current supplier after confirmation. + */ private void deleteSupplier() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () -> viewModel.deleteSupplier().observe(getViewLifecycleOwner(), resource -> { @@ -130,10 +154,16 @@ public class SupplierDetailFragment extends Fragment { })); } + /** + * Navigates back to the previous fragment. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + /** + * Handles fragment arguments to determine mode (new vs edit). + */ private void handleArguments() { if (getArguments() != null && getArguments().containsKey("supId")) { viewModel.setSupId(getArguments().getLong("supId")); @@ -144,6 +174,9 @@ public class SupplierDetailFragment extends Fragment { viewModel.setSupId(-1); } + /** + * Loads the details of the specified supplier. + */ private void loadSupplierData() { viewModel.loadSupplier().observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -154,6 +187,9 @@ public class SupplierDetailFragment extends Fragment { }); } + /** + * Applies the current view state to the UI components. + */ private void applyViewState(SupplierDetailViewModel.ViewState state) { binding.tvMode.setText(state.modeTitle); binding.tvSupId.setText(DateTimeUtils.formatId(viewModel.getSupId())); @@ -175,6 +211,9 @@ public class SupplierDetailFragment extends Fragment { updateIfDifferent(binding.etSupPhone, state.supPhone); } + /** + * Updates an EditText field only if the new value is different from the current one. + */ private void updateIfDifferent(EditText field, String value) { String current = field.getText() != null ? field.getText().toString() : ""; String next = value != null ? value : ""; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 496304a3..7cfa0910 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -36,6 +36,9 @@ import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; +/** + * Fragment for displaying and managing a pet's profile. + */ @AndroidEntryPoint public class PetProfileFragment extends Fragment { @@ -49,6 +52,9 @@ public class PetProfileFragment extends Fragment { private PetProfileViewModel viewModel; private ImagePickerHelper imagePickerHelper; + /** + * Initializes the ViewModel and image picker helper. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -67,6 +73,9 @@ public class PetProfileFragment extends Fragment { }); } + /** + * Inflates the layout, handles arguments, and sets up click listeners. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -95,18 +104,27 @@ public class PetProfileFragment extends Fragment { return binding.getRoot(); } + /** + * Shows or hides the loading progress bar. + */ private void setLoading(boolean loading) { if (binding != null && binding.progressBar != null) { binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); } } + /** + * Cleans up the binding reference. + */ @Override public void onDestroyView() { super.onDestroyView(); binding = null; } + /** + * Loads pet data from the ViewModel and updates the UI. + */ private void loadPetData() { viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -153,6 +171,9 @@ public class PetProfileFragment extends Fragment { }); } + /** + * Loads the pet's image using Glide with authentication. + */ private void loadPetImage(int petId) { String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId); String token = tokenManager.getToken(); @@ -170,6 +191,9 @@ public class PetProfileFragment extends Fragment { }); } + /** + * Uploads a new image for the pet. + */ private void uploadPetImage(Uri uri) { try { File file = FileUtils.getFileFromUri(requireContext(), uri); @@ -193,6 +217,9 @@ public class PetProfileFragment extends Fragment { } } + /** + * Deletes the pet's current image. + */ private void deletePetImage() { viewModel.deletePetImage(petId).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Chat.java b/android/app/src/main/java/com/example/petstoremobile/models/Chat.java index a519093c..9d782b05 100644 --- a/android/app/src/main/java/com/example/petstoremobile/models/Chat.java +++ b/android/app/src/main/java/com/example/petstoremobile/models/Chat.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.models; +/** + * Model class representing a chat conversation. + */ public class Chat { private String chatId; private String customerName; diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Message.java b/android/app/src/main/java/com/example/petstoremobile/models/Message.java index e6547d23..2e0ff23e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/models/Message.java +++ b/android/app/src/main/java/com/example/petstoremobile/models/Message.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.models; +/** + * Model class representing a chat message. + */ public class Message { private Long id; private Long conversationId; diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Sale.java b/android/app/src/main/java/com/example/petstoremobile/models/Sale.java index ce305d58..c67126e1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/models/Sale.java +++ b/android/app/src/main/java/com/example/petstoremobile/models/Sale.java @@ -1,5 +1,8 @@ package com.example.petstoremobile.models; +/** + * Model class representing a sale transaction. + */ public class Sale { private int saleId; private String saleDate; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java index 6c8acddf..ac890c9a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java @@ -11,6 +11,9 @@ import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for retrieving activity logs from the ActivityLogApi. + */ @Singleton public class ActivityLogRepository extends BaseRepository { private final ActivityLogApi activityLogApi; @@ -21,6 +24,9 @@ public class ActivityLogRepository extends BaseRepository { this.activityLogApi = activityLogApi; } + /** + * Retrieves a list of activity logs with optional filtering. + */ public LiveData>> getActivityLogs(int limit, Long storeId, String role, String search, String startDate, String endDate) { return executeCall(activityLogApi.getActivityLogs(limit, storeId, role, search, startDate, endDate)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java index 8758357a..fd9e5e3c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java @@ -11,6 +11,9 @@ import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing adoption data through the AdoptionApi. + */ @Singleton public class AdoptionRepository extends BaseRepository { private final AdoptionApi adoptionApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java index 6c2c4cf8..3f20ff3e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java @@ -11,6 +11,9 @@ import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing service appointments. + */ @Singleton public class AppointmentRepository extends BaseRepository { private final AppointmentApi appointmentApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java index 6011bac8..3e8fa71a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java @@ -22,6 +22,9 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +/** + * Repository class for handling authentication-related operations. + */ @Singleton public class AuthRepository extends BaseRepository { private final AuthApi authApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/BaseRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/BaseRepository.java index cf98dfe8..f56067f4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/BaseRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/BaseRepository.java @@ -9,7 +9,7 @@ import com.example.petstoremobile.utils.RetrofitUtils; import retrofit2.Call; /** - * Base class for all repositories to provide common functionality for API calls. + * Base repository class providing common functionality for executing API calls and handling responses. */ public abstract class BaseRepository { protected final String TAG; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java index 8d11511b..f518615b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java @@ -10,6 +10,9 @@ import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing product and service categories. + */ @Singleton public class CategoryRepository extends BaseRepository { private final CategoryApi categoryApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java index ec8e32b6..4e5f3ada 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java @@ -23,7 +23,7 @@ import okhttp3.RequestBody; import okhttp3.ResponseBody; /** - * Repository for handling chat-related data operations. + * Repository class for managing chat messages and conversations. */ @Singleton public class ChatRepository extends BaseRepository { diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java index f97d1ca0..83f2289f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CouponRepository.java @@ -12,6 +12,9 @@ import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing coupon data. + */ @Singleton public class CouponRepository extends BaseRepository { private final CouponApi couponApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java index 65c9a79d..6067b625 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java @@ -13,6 +13,9 @@ import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing customer information. + */ @Singleton public class CustomerRepository extends BaseRepository { private final CustomerApi customerApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/EmployeeRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/EmployeeRepository.java index b6ddcc9c..745936e6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/EmployeeRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/EmployeeRepository.java @@ -10,6 +10,9 @@ import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing employee data and roles. + */ @Singleton public class EmployeeRepository extends BaseRepository { private final EmployeeApi employeeApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java index 94526d25..83e8b9c1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java @@ -11,6 +11,9 @@ import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing inventory and stock levels. + */ @Singleton public class InventoryRepository extends BaseRepository { private final InventoryApi inventoryApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index 11bb9ff1..f035fa92 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -16,6 +16,9 @@ import javax.inject.Singleton; import okhttp3.MultipartBody; +/** + * Repository class for managing pet data through the PetApi. + */ @Singleton public class PetRepository extends BaseRepository { private final PetApi petApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java index 636e1430..1fca4df1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java @@ -15,6 +15,9 @@ import javax.inject.Singleton; import okhttp3.MultipartBody; +/** + * Repository class for managing product information and inventory levels. + */ @Singleton public class ProductRepository extends BaseRepository { private final ProductApi productApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java index 9b2f8df3..41d0393f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java @@ -11,6 +11,9 @@ import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing relationships between products and their suppliers. + */ @Singleton public class ProductSupplierRepository extends BaseRepository { private final ProductSupplierApi api; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java index dd9bd637..70537ade 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java @@ -10,6 +10,9 @@ import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing purchase orders with suppliers. + */ @Singleton public class PurchaseOrderRepository extends BaseRepository { private final PurchaseOrderApi purchaseOrderApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java index 182a7f0e..4fb697a0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SaleRepository.java @@ -10,6 +10,9 @@ import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing sale transactions and refund data. + */ @Singleton public class SaleRepository extends BaseRepository { private final SaleApi saleApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java index bd5f3ebc..98e8b457 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java @@ -11,6 +11,9 @@ import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing pet service offerings. + */ @Singleton public class ServiceRepository extends BaseRepository { private final ServiceApi serviceApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java index 0df93ab1..dca03260 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java @@ -13,6 +13,9 @@ import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for retrieving store-related information. + */ @Singleton public class StoreRepository extends BaseRepository { private final StoreApi storeApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java index 7aef86a2..bfdf9e39 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java @@ -11,6 +11,9 @@ import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing supplier data. + */ @Singleton public class SupplierRepository extends BaseRepository { private final SupplierApi supplierApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/UserRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/UserRepository.java index 0ed9ced9..bfa9cc23 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/UserRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/UserRepository.java @@ -10,6 +10,9 @@ import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Singleton; +/** + * Repository class for managing user profile and account data. + */ @Singleton public class UserRepository extends BaseRepository { private final UserApi userApi; diff --git a/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java b/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java index 90113cac..46fc5f4d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java +++ b/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java @@ -30,7 +30,9 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; -// Service to receive notifications when a new conversation is created +/** + * Service to receive and display notifications when a new chat conversation or message is created. + */ @AndroidEntryPoint public class ChatNotificationService extends Service { private static final String TAG = "ChatNotificationService"; diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/EventDecorator.java b/android/app/src/main/java/com/example/petstoremobile/utils/EventDecorator.java index b58f38d4..4f3e62ad 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/EventDecorator.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/EventDecorator.java @@ -8,11 +8,17 @@ import com.prolificinteractive.materialcalendarview.spans.DotSpan; import java.util.Collection; import java.util.HashSet; +/** + * Decorator for MaterialCalendarView to highlight specific dates with a colored dot. + */ public class EventDecorator implements DayViewDecorator { private final int color; private final HashSet dates; + /** + * Initializes the decorator with a color and a collection of dates to highlight. + */ public EventDecorator(int color, Collection dates) { this.color = color; this.dates = new HashSet<>(dates); diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java index bf45f4f8..174b23f5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java @@ -8,7 +8,14 @@ import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; +/** + * Utility class for file operations, particularly handling Uris from the Android content system. + */ public class FileUtils { + /** + * Creates a temporary file in the cache directory from a given Uri. + * This allows the app to work with a local file path instead of a content stream. + */ public static File getFileFromUri(Context context, Uri uri) { try { if ("content".equals(uri.getScheme())) { @@ -48,6 +55,9 @@ public class FileUtils { } } + /** + * Retrieves the display name of a file from its Uri. + */ public static String getFileName(Context context, Uri uri) { String result = null; if (uri.getScheme().equals("content")) { diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java b/android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java index 197cb557..f651c25a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java @@ -5,7 +5,7 @@ import java.util.List; /** * Helper class to manage selection state in Adapters for bulk operations. - * Uses String keys to support both simple Long IDs and composite keys (e.g., "id1-id2"). + * Uses String keys to support both Long IDs and composite keys. */ public class SelectionHelper { @@ -13,15 +13,32 @@ public class SelectionHelper { private boolean selectionMode = false; private final SelectionListener listener; + /** + * Listener interface to observe selection state changes. + */ public interface SelectionListener { + /** + * Called when the number of selected items changes. + */ void onSelectionChanged(int count); + + /** + * Called when selection mode is enabled or disabled. + */ void onSelectionModeToggle(boolean selectionMode); } + /** + * Initializes the helper with a listener. + */ public SelectionHelper(SelectionListener listener) { this.listener = listener; } + /** + * Toggles the selection state of a specific key. + * Automatically exits selection mode if the last item is deselected. + */ public void toggleSelection(String key) { if (key == null) return; @@ -39,6 +56,9 @@ public class SelectionHelper { } } + /** + * Enters selection mode and selects the specified key. + */ public void startSelection(String key) { if (key == null) return; selectionMode = true; @@ -47,18 +67,30 @@ public class SelectionHelper { listener.onSelectionModeToggle(true); } + /** + * Checks if a specific key is currently selected. + */ public boolean isSelected(String key) { return selectedKeys.contains(key); } + /** + * Checks if the helper is currently in selection mode. + */ public boolean isInSelectionMode() { return selectionMode; } + /** + * Returns a copy of the list of currently selected keys. + */ public List getSelectedKeys() { return new ArrayList<>(selectedKeys); } + /** + * Clears all selections and exits selection mode. + */ public void clearSelection() { selectedKeys.clear(); selectionMode = false; diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java index de1e6304..2273b227 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java @@ -22,7 +22,7 @@ import java.util.function.Function; public class SpinnerUtils { /** - * Populates a spinner with a list of items and handles pre-selection. + * Populates a spinner with generic data and handles pre-selection based on an ID. */ public static void populateSpinner(Context context, Spinner spinner, List data, Function nameExtractor, String defaultText, @@ -31,7 +31,7 @@ public class SpinnerUtils { } /** - * Populates a spinner with white text (for dark backgrounds). + * Populates a spinner with white text, suitable for dark backgrounds or overlays. */ public static void populateWhiteSpinner(Context context, Spinner spinner, List data, Function nameExtractor, String defaultText, @@ -39,6 +39,9 @@ public class SpinnerUtils { populateSpinnerWithAdapter(context, spinner, data, nameExtractor, defaultText, preselectedId, idExtractor, true); } + /** + * Internal helper to populate a spinner with the appropriate adapter and handle pre-selection. + */ private static void populateSpinnerWithAdapter(Context context, Spinner spinner, List data, Function nameExtractor, String defaultText, Long preselectedId, Function idExtractor, diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java index d944de83..453ddd3b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java @@ -44,16 +44,37 @@ public class ActivityLogListViewModel extends ViewModel { this.storeRepository = storeRepository; } + /** + * Returns the LiveData for the list of activity logs. + */ public LiveData> getLogs() { return logs; } + + /** + * Returns the LiveData for the store dropdown options. + */ public LiveData> getStoreOptions() { return storeOptions; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of logs has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads initial data for stores and logs. + */ public void loadInitialData() { loadStores(); loadLogs(true); } + /** + * Loads store options for filtering. + */ private void loadStores() { observeOnce(storeRepository.getStoreDropdowns(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -62,6 +83,9 @@ public class ActivityLogListViewModel extends ViewModel { }); } + /** + * Loads activity logs from the repository with current filters. + */ public void loadLogs(boolean reset) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -88,21 +112,33 @@ public class ActivityLogListViewModel extends ViewModel { }); } + /** + * Sets the role filter and reloads logs. + */ public void setRoleFilter(String role) { currentRole = "All Roles".equals(role) ? null : role; loadLogs(true); } + /** + * Sets the store filter and reloads logs. + */ public void setStoreFilter(Long storeId) { currentStoreId = storeId; loadLogs(true); } + /** + * Sets the search query and reloads logs. + */ public void setSearchQuery(String query) { currentSearch = (query == null || query.trim().isEmpty()) ? null : query.trim(); loadLogs(true); } + /** + * Sets the date range filter and reloads logs. + */ public void setDateRange(String startDate, String endDate) { currentStartDate = startDate; currentEndDate = endDate; @@ -110,6 +146,9 @@ public class ActivityLogListViewModel extends ViewModel { } + /** + * Observes a LiveData once, removing the observer after the first response. + */ private void observeOnce(LiveData> liveData, Observer> handler) { liveData.observeForever(new Observer>() { @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java index 7416c0fc..c25ce797 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionDetailViewModel.java @@ -48,24 +48,39 @@ public class AdoptionDetailViewModel extends ViewModel { this.storeRepository = storeRepository; } + /** + * Sets the adoption ID and initializes the view mode. + */ public void setAdoptionId(long id) { this.adoptionId = id; initMode(id != -1); } + /** + * Returns the current adoption ID. + */ public long getAdoptionId() { return adoptionId; } + /** + * Checks if the fragment is currently in editing mode. + */ public boolean isEditing() { ViewState current = viewState.getValue(); return current != null && current.isEditing; } + /** + * Returns the LiveData for the current view state. + */ public LiveData getViewState() { return viewState; } + /** + * Initializes the view state based on whether the mode is editing or adding. + */ public void initMode(boolean isEditing) { updateViewState(state -> { state.isEditing = isEditing; @@ -94,6 +109,9 @@ public class AdoptionDetailViewModel extends ViewModel { }); } + /** + * Loads initial data for the form spinners. + */ public void loadInitialFormData(boolean isEditing) { // Pets are loaded dynamically based on store selection; no pre-load needed. observeOnce(customerRepository.getCustomerDropdowns(), r -> { @@ -108,6 +126,9 @@ public class AdoptionDetailViewModel extends ViewModel { }); } + /** + * Handles customer selection and updates the view state. + */ public void onCustomerSelected(int position) { List list = customerList.getValue(); Long customerId = (position > 0 && list != null && position <= list.size()) @@ -115,6 +136,9 @@ public class AdoptionDetailViewModel extends ViewModel { updateViewState(state -> state.selectedCustomerId = customerId); } + /** + * Handles store selection, updates the view state, and loads related employees and pets. + */ public void onStoreSelected(int position) { List list = storeList.getValue(); if (position > 0 && list != null && position <= list.size()) { @@ -141,6 +165,9 @@ public class AdoptionDetailViewModel extends ViewModel { } } + /** + * Handles pet selection, updates the view state, and loads the pet's price. + */ public void onPetSelected(int position) { List list = petList.getValue(); if (position > 0 && list != null && position <= list.size()) { @@ -155,6 +182,9 @@ public class AdoptionDetailViewModel extends ViewModel { } } + /** + * Loads available pets for the selected store. + */ private void loadAvailablePetsByStore(Long storeId) { observeOnce(petRepository.getAdoptionPets(storeId), r -> { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { @@ -163,6 +193,9 @@ public class AdoptionDetailViewModel extends ViewModel { }); } + /** + * Loads the price for a specific pet. + */ private void loadPetPrice(Long petId) { observeOnce(petRepository.getPetById(petId), r -> { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { @@ -181,6 +214,9 @@ public class AdoptionDetailViewModel extends ViewModel { }); } + /** + * Loads employees assigned to a specific store. + */ private void loadEmployeesForStore(Long storeId) { observeOnce(storeRepository.getStoreEmployees(storeId), r -> { if (r != null && r.status == Resource.Status.SUCCESS && r.data != null) { @@ -190,7 +226,7 @@ public class AdoptionDetailViewModel extends ViewModel { } /** - * Called when the date or status changes in the UI. Applies date-based field enabling. + * Called when the date or status changes in the UI. */ public void onDateChanged(String date, String currentStatus) { updateViewState(s -> { @@ -203,7 +239,7 @@ public class AdoptionDetailViewModel extends ViewModel { s.selectedStatus = s.availableStatuses[0]; } - if (!s.isEditing) return; // add mode: field enabling handled separately + if (!s.isEditing) return; boolean isPast = DateTimeUtils.isDateBeforeToday(date); if (isPast) { @@ -222,6 +258,9 @@ public class AdoptionDetailViewModel extends ViewModel { }); } + /** + * Calculates available statuses based on the date and mode. + */ private String[] calculateAvailableStatuses(boolean isEditing, String date) { if (!isEditing) return new String[]{"Pending"}; if (date == null || date.isEmpty()) return new String[]{}; @@ -236,9 +275,11 @@ public class AdoptionDetailViewModel extends ViewModel { s.isPetEnabled = enabled; s.isEmployeeEnabled = enabled; s.isDateEnabled = enabled; - // fee never editable } + /** + * Fetches adoption details from the repository. + */ public LiveData> loadAdoption() { MutableLiveData> result = new MutableLiveData<>(); observeOnce(adoptionRepository.getAdoptionById(adoptionId), resource -> { @@ -292,6 +333,9 @@ public class AdoptionDetailViewModel extends ViewModel { return result; } + /** + * Saves or updates the adoption record. + */ public LiveData> saveAdoption(AdoptionDTO dto) { if (isEditing()) { return adoptionRepository.updateAdoption(adoptionId, dto); @@ -299,17 +343,41 @@ public class AdoptionDetailViewModel extends ViewModel { return adoptionRepository.createAdoption(dto); } + /** + * Deletes the current adoption record. + */ public LiveData> deleteAdoption() { return adoptionRepository.deleteAdoption(adoptionId); } + /** + * Returns the LiveData for the pet dropdown list. + */ public LiveData> getPetList() { return petList; } + + /** + * Returns the LiveData for the customer dropdown list. + */ public LiveData> getCustomerList() { return customerList; } + + /** + * Returns the LiveData for the store dropdown list. + */ public LiveData> getStoreList() { return storeList; } + + /** + * Returns the LiveData for the employee dropdown list. + */ public LiveData> getEmployeeList() { return employeeList; } + /** + * Updates the employee list. + */ public void setEmployeeList(List list) { employeeList.setValue(list); } + /** + * Helper to update the view state atomically. + */ private void updateViewState(Action action) { ViewState current = viewState.getValue(); if (current != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java index 005198d1..dbfc262b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionListViewModel.java @@ -39,11 +39,29 @@ public class AdoptionListViewModel extends ViewModel { this.storeRepository = storeRepository; } + /** + * Returns the LiveData for the list of adoptions. + */ public LiveData> getAdoptions() { return adoptions; } + + /** + * Returns the LiveData for the list of stores for filtering. + */ public LiveData> getStores() { return stores; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of adoptions has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads adoptions from the repository with the specified filters. + */ public void loadAdoptions(boolean reset, String query, String status, Long storeId, String date, Long employeeId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -73,6 +91,9 @@ public class AdoptionListViewModel extends ViewModel { }); } + /** + * Loads the list of stores for filtering options. + */ public void loadStores() { observeOnce(storeRepository.getAllStores(0, 100), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -93,6 +114,9 @@ public class AdoptionListViewModel extends ViewModel { }); } + /** + * Deletes multiple adoptions by their IDs. + */ public LiveData> bulkDeleteAdoptions(List ids) { return adoptionRepository.bulkDeleteAdoptions(new BulkDeleteRequest(ids)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java index 4a9b94d9..95c8979f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AnalyticsViewModel.java @@ -28,6 +28,9 @@ import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; +/** + * ViewModel for retrieving and providing data for the analytics dashboard. + */ @HiltViewModel public class AnalyticsViewModel extends ViewModel { private final SaleRepository saleRepository; @@ -50,12 +53,34 @@ public class AnalyticsViewModel extends ViewModel { this.tokenManager = tokenManager; } + /** + * Returns the LiveData for the analytics data. + */ public LiveData getAnalyticsData() { return analyticsData; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Returns the LiveData for error messages. + */ public LiveData getErrorMessage() { return errorMessage; } + + /** + * Returns the LiveData for available payment method filters. + */ public LiveData> getAvailablePaymentMethods() { return availablePaymentMethods; } + + /** + * Returns the LiveData for available store filters. + */ public LiveData> getAvailableStores() { return availableStores; } + /** + * Triggers loading of analytics data with current filters. + */ public void loadAnalytics() { isLoading.setValue(true); errorMessage.setValue(null); @@ -75,35 +100,56 @@ public class AnalyticsViewModel extends ViewModel { }); } + /** + * Applies a new filter state and re-computes analytics. + */ public void applyFilter(FilterState filter) { currentFilter = filter; applyCurrentFilter(); } + /** + * Resets filters to default values and reset analytics. + */ public void resetFilter() { currentFilter = new FilterState(); storeFilter = "All Stores"; applyCurrentFilter(); } + /** + * Sets the view mode + */ public void setViewMode(String mode) { viewMode = mode; applyCurrentFilter(); } + /** + * Returns the current view mode. + */ public String getViewMode() { return viewMode; } + /** + * Sets the store filter + */ public void setStoreFilter(String store) { storeFilter = (store != null && !store.isEmpty()) ? store : "All Stores"; applyCurrentFilter(); } + /** + * Returns the current store filter. + */ public String getStoreFilter() { return storeFilter; } + /** + * Applies the current filters + */ private void applyCurrentFilter() { List salesForMode; if (viewMode.equals("mine")) { @@ -124,6 +170,9 @@ public class AnalyticsViewModel extends ViewModel { computeAnalytics(filtered, currentFilter); } + /** + * Extracts unique store names from the cached sales data for filtering. + */ private void deriveStores() { java.util.Set stores = new java.util.TreeSet<>(); for (SaleDTO s : cachedSales) { @@ -137,6 +186,9 @@ public class AnalyticsViewModel extends ViewModel { availableStores.setValue(result); } + /** + * Extracts unique payment methods from the cached sales data for filtering. + */ private void derivePaymentMethods() { java.util.Set methods = new java.util.TreeSet<>(); for (SaleDTO s : cachedSales) { @@ -150,6 +202,9 @@ public class AnalyticsViewModel extends ViewModel { availablePaymentMethods.setValue(result); } + /** + * Filters a list of sales based on date range and payment method. + */ private List filterSales(List sales, FilterState filter) { List result = new ArrayList<>(); for (SaleDTO s : sales) { @@ -165,6 +220,9 @@ public class AnalyticsViewModel extends ViewModel { return result; } + /** + * Computesanalytics data from a filtered list of sales. + */ private void computeAnalytics(List sales, FilterState filter) { List regularSales = new ArrayList<>(); for (SaleDTO s : sales) { @@ -258,6 +316,9 @@ public class AnalyticsViewModel extends ViewModel { analyticsData.setValue(data); } + /** + * Returns a date string for the specified offset from today. + */ private String todayString(int offsetDays) { Calendar c = Calendar.getInstance(); c.add(Calendar.DAY_OF_YEAR, offsetDays); @@ -265,6 +326,9 @@ public class AnalyticsViewModel extends ViewModel { c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH)); } + /** + * Shifts a date string by a specified number of days. + */ private String shiftDate(String date, int offsetDays) { try { String[] p = date.split("-"); @@ -278,6 +342,9 @@ public class AnalyticsViewModel extends ViewModel { } } + /** + * Generates a list of date strings between the start and end dates. + */ private List buildDateRange(String start, String end, int maxDays) { List dates = new ArrayList<>(); try { @@ -298,6 +365,9 @@ public class AnalyticsViewModel extends ViewModel { return dates; } + /** + * Builds a title for the daily revenue chart based on the date range. + */ private String buildDailyTitle(FilterState filter, String rangeStart, String rangeEnd) { if (filter.startDate.isEmpty() && filter.endDate.isEmpty()) return "Daily Revenue (Last 7 Days)"; String s = rangeStart.length() >= 10 ? rangeStart.substring(5) : rangeStart; @@ -305,6 +375,9 @@ public class AnalyticsViewModel extends ViewModel { return "Daily Revenue (" + s + " – " + e + ")"; } + /** + * Observes a LiveData once, removing the observer after the first non-loading response. + */ private void observeOnce(LiveData> liveData, Observer> handler) { liveData.observeForever(new Observer>() { @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java index b229c3a0..c96e21de 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentDetailViewModel.java @@ -387,7 +387,9 @@ public class AppointmentDetailViewModel extends ViewModel { void run(T t); } - /** Observes a LiveData once, removing the observer after the first non-loading response. */ + /** + * Observes a LiveData once, removing the observer after the first non-loading response. + */ private void observeOnce(LiveData> liveData, Observer> handler) { liveData.observeForever(new Observer>() { @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java index 19924349..30aa2511 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentListViewModel.java @@ -39,11 +39,29 @@ public class AppointmentListViewModel extends ViewModel { this.storeRepository = storeRepository; } + /** + * Returns the LiveData for the list of appointments. + */ public LiveData> getAppointments() { return appointments; } + + /** + * Returns the LiveData for the list of stores for filtering. + */ public LiveData> getStores() { return stores; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of appointments has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads appointments from the repository with the specified filters. + */ public void loadAppointments(boolean reset, String query, String status, Long storeId, String date, Long employeeId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -71,6 +89,9 @@ public class AppointmentListViewModel extends ViewModel { }); } + /** + * Loads the list of stores for filtering options. + */ public void loadStores() { observeOnce(storeRepository.getAllStores(0, 100), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -91,6 +112,9 @@ public class AppointmentListViewModel extends ViewModel { }); } + /** + * Deletes multiple appointments by their IDs. + */ public LiveData> bulkDeleteAppointments(List ids) { return appointmentRepository.bulkDeleteAppointments(new BulkDeleteRequest(ids)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java index 36e437bb..cc05eb3e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java @@ -16,6 +16,9 @@ import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; import okhttp3.MultipartBody; +/** + * ViewModel for managing user authentication and profile data. + */ @HiltViewModel public class AuthViewModel extends ViewModel { private final AuthRepository repository; @@ -26,42 +29,42 @@ public class AuthViewModel extends ViewModel { } /** - * Authenticates a user with username and password. + * Attempts to log in the user with the provided credentials. */ public LiveData> login(String username, String password) { return repository.login(new AuthDTO.LoginRequest(username, password)); } /** - * Retrieves the profile information of the currently authenticated user. + * Retrieves the current user's profile information. */ public LiveData> getMe() { return repository.getMe(); } /** - * Updates the profile information of the current user. + * Updates the current user's profile information. */ public LiveData> updateMe(Map updates) { return repository.updateMe(updates); } /** - * Uploads a new avatar image for the current user. + * Uploads a new avatar for the current user. */ public LiveData> uploadAvatar(MultipartBody.Part avatar) { return repository.uploadAvatar(avatar); } /** - * Deletes the avatar image of the current user. + * Deletes the current user's avatar. */ public LiveData> deleteAvatar() { return repository.deleteAvatar(); } /** - * Logs out the current user by clearing stored credentials. + * Logs out the user by clearing the session. */ public void logout() { repository.logout(); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java index 4d14a3b4..0010d227 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java @@ -29,6 +29,9 @@ import okhttp3.MultipartBody; import okhttp3.RequestBody; import okhttp3.ResponseBody; +/** + * ViewModel for managing the list of chat conversations and real-time message updates. + */ @HiltViewModel public class ChatListViewModel extends ViewModel { private final ChatRepository chatRepository; @@ -48,19 +51,43 @@ public class ChatListViewModel extends ViewModel { this.customerRepository = customerRepository; } + /** + * Returns the LiveData for the list of active chats. + */ public LiveData> getActiveChats() { return activeChats; } + + /** + * Returns the LiveData for the list of closed chats. + */ public LiveData> getClosedChats() { return closedChats; } + + /** + * Returns the LiveData for the list of messages in the current conversation. + */ public LiveData> getMessageList() { return messageList; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + /** + * Returns the ID of the last active conversation. + */ public Long getLastActiveConversationId() { return lastActiveConversationId; } + /** + * Sets the ID of the last active conversation. + */ public void setLastActiveConversationId(Long conversationId) { this.lastActiveConversationId = conversationId; } + /** + * Loads the list of customers for name mapping. + */ public void loadCustomers() { isLoading.setValue(true); customerRepository.getAllCustomers(0, 100).observeForever(resource -> { @@ -75,6 +102,9 @@ public class ChatListViewModel extends ViewModel { }); } + /** + * Loads all chat conversations from the repository. + */ public void loadConversations() { isLoading.setValue(true); chatRepository.getAllConversations().observeForever(resource -> { @@ -99,6 +129,9 @@ public class ChatListViewModel extends ViewModel { }); } + /** + * Loads message history for a specific conversation. + */ public void loadMessageHistory(Long conversationId) { isLoading.setValue(true); chatRepository.getMessages(conversationId).observeForever(resource -> { @@ -115,22 +148,37 @@ public class ChatListViewModel extends ViewModel { }); } + /** + * Sends a text message in a specific conversation. + */ public LiveData> sendMessage(Long conversationId, String text) { return chatRepository.sendMessage(conversationId, new SendMessageRequest(text)); } + /** + * Sends a message with an attachment in a specific conversation. + */ public LiveData> sendMessageWithAttachment(Long conversationId, MultipartBody.Part content, MultipartBody.Part attachment) { return chatRepository.sendMessageWithAttachment(conversationId, content, attachment); } + /** + * Downloads an attachment for a specific message. + */ public LiveData> downloadAttachment(Long messageId) { return chatRepository.downloadAttachment(messageId); } + /** + * Closes an active chat conversation. + */ public LiveData> closeConversation(Long conversationId) { return chatRepository.updateConversationStatus(conversationId, new UpdateConversationStatusRequest("CLOSED")); } + /** + * Adds a message to the local list (used for real-time updates). + */ public void addMessageLocally(MessageDTO dto) { List current = new ArrayList<>(messageList.getValue()); if (dto.getId() != null) { @@ -142,6 +190,9 @@ public class ChatListViewModel extends ViewModel { messageList.setValue(current); } + /** + * Updates a conversation's status locally. + */ public void updateConversationLocally(ConversationDTO dto) { updateList(activeChats, dto); updateList(closedChats, dto); @@ -180,6 +231,9 @@ public class ChatListViewModel extends ViewModel { return m; } + /** + * Retrieves a customer's name by their ID. + */ public String getCustomerName(Long customerId) { return customerNames.getOrDefault(customerId, "Customer #" + customerId); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponDetailViewModel.java index 49f52ef5..93541676 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponDetailViewModel.java @@ -23,23 +23,38 @@ public class CouponDetailViewModel extends ViewModel { this.repository = repository; } + /** + * Loads the coupon details from the repository. + */ public LiveData> loadCoupon(long id) { return repository.getCouponById(id); } + /** + * Sets the current coupon ID and editing mode. + */ public void setCouponId(long id, boolean isEditing) { this.couponId = id; this.isEditing = isEditing; } + /** + * Returns the current coupon ID. + */ public long getCouponId() { return couponId; } + /** + * Checks if the fragment is in editing mode. + */ public boolean isEditing() { return isEditing; } + /** + * Saves or updates the coupon record. + */ public LiveData> saveCoupon(CouponDTO dto) { if (isEditing && couponId > 0) { return repository.updateCoupon(couponId, dto); @@ -48,6 +63,9 @@ public class CouponDetailViewModel extends ViewModel { } } + /** + * Deletes the current coupon record. + */ public LiveData> deleteCoupon() { return repository.deleteCoupon(couponId); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java index d825b935..eeb47f16 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CouponListViewModel.java @@ -17,6 +17,9 @@ import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; +/** + * ViewModel for managing the list of coupons. + */ @HiltViewModel public class CouponListViewModel extends ViewModel { private final CouponRepository repository; @@ -32,10 +35,24 @@ public class CouponListViewModel extends ViewModel { this.repository = repository; } + /** + * Returns the LiveData for the list of coupons. + */ public LiveData> getCoupons() { return coupons; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of coupons has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads coupons from the repository with the specified filters. + */ public void loadCoupons(boolean reset, Boolean active, String discountType, String sort) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -75,6 +92,9 @@ public class CouponListViewModel extends ViewModel { }); } + /** + * Deletes multiple coupons by their IDs. + */ public LiveData> bulkDeleteCoupons(List ids) { return repository.bulkDeleteCoupons(ids); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java index 0d0ebfaf..1604c9e8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerDetailViewModel.java @@ -23,6 +23,9 @@ public class CustomerDetailViewModel extends ViewModel { this.repository = repository; } + /** + * Sets the current customer ID and editing mode. + */ public void setCustomerId(long id, boolean isEditing) { this.customerId = id; this.isEditing = isEditing; @@ -48,6 +51,9 @@ public class CustomerDetailViewModel extends ViewModel { return repository.getCustomerById(id); } + /** + * Saves or updates the customer record. + */ public LiveData> saveCustomer(CustomerDTO dto) { if (isEditing && customerId > 0) { return repository.updateCustomer(customerId, dto); @@ -56,6 +62,9 @@ public class CustomerDetailViewModel extends ViewModel { } } + /** + * Deletes the current customer record. + */ public LiveData> deleteCustomer() { return repository.deleteCustomer(customerId); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java index a216c47a..2fd7393b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerListViewModel.java @@ -17,6 +17,9 @@ import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; +/** + * ViewModel for managing the list of customers, including local filtering and pagination. + */ @HiltViewModel public class CustomerListViewModel extends ViewModel { private final CustomerRepository repository; @@ -37,10 +40,24 @@ public class CustomerListViewModel extends ViewModel { this.repository = repository; } + /** + * Returns the LiveData for the list of filtered customers. + */ public LiveData> getFilteredCustomers() { return filteredCustomers; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of customers has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads the full list of customers from the repository. + */ public void loadCustomers(boolean reset) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -69,6 +86,9 @@ public class CustomerListViewModel extends ViewModel { }); } + /** + * Observes a LiveData once, removing the observer after the first non-loading response. + */ private void observeOnce(LiveData> liveData, Observer> handler) { liveData.observeForever(new Observer>() { @Override @@ -81,12 +101,18 @@ public class CustomerListViewModel extends ViewModel { }); } + /** + * Filters the customer list locally based on query and status. + */ public void filter(String query, String status) { this.lastQuery = query; this.lastStatus = status; applyFilter(customers.getValue()); } + /** + * Applies the filters to the full list and updates the filtered LiveData. + */ private void applyFilter(List all) { if (all == null) return; diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java index c872ea49..3bf33373 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryDetailViewModel.java @@ -39,26 +39,48 @@ public class InventoryDetailViewModel extends ViewModel { this.productRepository = productRepository; } + /** + * Sets the inventory ID and initializes the editing mode. + */ public void setInventoryId(long id) { this.inventoryId = id; this.isEditing = id != -1; } + /** + * Returns the current inventory ID. + */ public long getInventoryId() { return inventoryId; } + + /** + * Checks if the fragment is in editing mode. + */ public boolean isEditing() { return isEditing; } + /** + * Loads the inventory record from the repository. + */ public LiveData> loadInventory() { return inventoryRepository.getInventoryById(inventoryId); } + /** + * Loads store dropdown options. + */ public LiveData>> loadStores() { return storeRepository.getStoreDropdowns(); } + /** + * Loads product dropdown options. + */ public LiveData>> loadProducts() { return productRepository.getProductDropdowns(); } + /** + * Saves or updates the inventory record. + */ public LiveData> saveInventory(InventoryDTO dto) { if (isEditing) { return inventoryRepository.updateInventory(inventoryId, dto); @@ -67,13 +89,30 @@ public class InventoryDetailViewModel extends ViewModel { } } + /** + * Deletes the current inventory record. + */ public LiveData> deleteInventory() { return inventoryRepository.deleteInventory(inventoryId); } + /** + * Updates the store list. + */ public void setStoreList(List list) { storeList.setValue(list); } + + /** + * Returns the LiveData for the store list. + */ public LiveData> getStoreList() { return storeList; } + /** + * Updates the product list. + */ public void setProductList(List list) { productList.setValue(list); } + + /** + * Returns the LiveData for the product list. + */ public LiveData> getProductList() { return productList; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java index 66387983..8e482390 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryListViewModel.java @@ -39,11 +39,29 @@ public class InventoryListViewModel extends ViewModel { this.storeRepository = storeRepository; } + /** + * Returns the LiveData for the inventory list. + */ public LiveData> getInventory() { return inventory; } + + /** + * Returns the LiveData for the store list for filtering. + */ public LiveData> getStores() { return stores; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of inventory has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads inventory items from the repository with the specified filters. + */ public void loadInventory(boolean reset, String query, Long storeId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -69,6 +87,9 @@ public class InventoryListViewModel extends ViewModel { }); } + /** + * Loads the list of stores for filtering options. + */ public void loadStores() { observeOnce(storeRepository.getAllStores(0, 100), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -89,6 +110,9 @@ public class InventoryListViewModel extends ViewModel { }); } + /** + * Deletes multiple inventory items by their IDs. + */ public LiveData> bulkDeleteInventory(List ids) { return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java index 89f56d51..5c04e468 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetListViewModel.java @@ -21,6 +21,9 @@ import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; +/** + * ViewModel for managing the list of pets, including filtering and bulk deletion. + */ @HiltViewModel public class PetListViewModel extends ViewModel { private final PetRepository petRepository; @@ -41,12 +44,34 @@ public class PetListViewModel extends ViewModel { this.storeRepository = storeRepository; } + /** + * Returns the LiveData for the list of pets. + */ public LiveData> getPets() { return pets; } + + /** + * Returns the LiveData for the list of stores for filtering. + */ public LiveData> getStores() { return stores; } + + /** + * Returns the LiveData for available species filters. + */ public LiveData> getSpeciesOptions() { return speciesOptions; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of pets has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads pets from the repository with the specified filters. + */ public void loadPets(boolean reset, String query, String status, String species, Long storeId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -79,6 +104,9 @@ public class PetListViewModel extends ViewModel { }); } + /** + * Loads the list of available species for filtering. + */ public void loadSpecies() { observeOnce(petRepository.getPetSpeciesDropdowns(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -92,6 +120,9 @@ public class PetListViewModel extends ViewModel { }); } + /** + * Loads the list of stores for filtering options. + */ public void loadStores() { observeOnce(storeRepository.getAllStores(0, 100), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -100,6 +131,9 @@ public class PetListViewModel extends ViewModel { }); } + /** + * Observes a LiveData once, removing the observer after the first non-loading response. + */ private void observeOnce(LiveData> liveData, Observer> handler) { liveData.observeForever(new Observer>() { @Override @@ -112,6 +146,9 @@ public class PetListViewModel extends ViewModel { }); } + /** + * Deletes multiple pets by their IDs. + */ public LiveData> bulkDeletePets(List ids) { return petRepository.bulkDeletePets(new BulkDeleteRequest(ids)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java index 1fb75f1c..8366bf63 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetProfileViewModel.java @@ -21,14 +21,23 @@ public class PetProfileViewModel extends ViewModel { this.repository = repository; } + /** + * Retrieves a pet's details by its ID. + */ public LiveData> getPetById(Long id) { return repository.getPetById(id); } + /** + * Uploads an image for a specific pet. + */ public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { return repository.uploadPetImage(id, image); } + /** + * Deletes the image for a specific pet. + */ public LiveData> deletePetImage(Long id) { return repository.deletePetImage(id); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java index c1ac5fdd..3c157dde 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductDetailViewModel.java @@ -35,27 +35,45 @@ public class ProductDetailViewModel extends ViewModel { this.categoryRepository = categoryRepository; } + /** + * Sets the product ID and initializes the editing mode. + */ public void setProdId(long id) { this.prodId = id; this.isEditing = id != -1; } + /** + * Returns the current product ID. + */ public long getProdId() { return prodId; } + /** + * Checks if the fragment is in editing mode. + */ public boolean isEditing() { return isEditing; } + /** + * Loads product category dropdown options. + */ public LiveData>> loadCategories() { return productRepository.getCategoryDropdowns(); } + /** + * Loads the product details from the repository. + */ public LiveData> loadProduct() { return productRepository.getProductById(prodId); } + /** + * Saves or updates the product record. + */ public LiveData> saveProduct(ProductDTO dto) { if (isEditing) { return productRepository.updateProduct(prodId, dto); @@ -64,22 +82,37 @@ public class ProductDetailViewModel extends ViewModel { } } + /** + * Deletes the current product record. + */ public LiveData> deleteProduct() { return productRepository.deleteProduct(prodId); } + /** + * Uploads an image for the current product. + */ public LiveData> uploadProductImage(MultipartBody.Part image) { return productRepository.uploadProductImage(prodId, image); } + /** + * Deletes the current product's image. + */ public LiveData> deleteProductImage() { return productRepository.deleteProductImage(prodId); } + /** + * Updates the category list. + */ public void setCategoryList(List list) { categoryList.setValue(list); } + /** + * Returns the LiveData for the category list. + */ public LiveData> getCategoryList() { return categoryList; } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java index b22dee6c..e242768f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductListViewModel.java @@ -38,11 +38,29 @@ public class ProductListViewModel extends ViewModel { this.categoryRepository = categoryRepository; } + /** + * Returns the LiveData for the list of products. + */ public LiveData> getProducts() { return products; } + + /** + * Returns the LiveData for the list of categories for filtering. + */ public LiveData> getCategories() { return categories; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of products has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads products from the repository with the specified filters. + */ public void loadProducts(boolean reset, String query, Long categoryId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -70,6 +88,9 @@ public class ProductListViewModel extends ViewModel { }); } + /** + * Loads the list of categories for filtering options. + */ public void loadCategories() { observeOnce(productRepository.getCategoryDropdowns(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java index 2cbf5036..66d0201e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierDetailViewModel.java @@ -40,28 +40,54 @@ public class ProductSupplierDetailViewModel extends ViewModel { this.supplierRepository = supplierRepository; } + /** + * Sets the composite key for the product-supplier relationship and enables editing mode. + */ public void setEditMode(long productId, long supplierId) { this.isEditing = true; this.editProductId = productId; this.editSupplierId = supplierId; } + /** + * Loads the product-supplier details from the repository. + */ public LiveData> loadProductSupplier() { return psRepository.getProductSupplierById(editProductId, editSupplierId); } + /** + * Checks if the fragment is in editing mode. + */ public boolean isEditing() { return isEditing; } + + /** + * Returns the product ID for the relationship being edited. + */ public long getEditProductId() { return editProductId; } + + /** + * Returns the supplier ID for the relationship being edited. + */ public long getEditSupplierId() { return editSupplierId; } + /** + * Loads products for the dropdown selection. + */ public LiveData>> loadProducts() { return productRepository.getAllProducts(null, null, 0, 200, "prodName"); } + /** + * Loads suppliers for the dropdown selection. + */ public LiveData>> loadSuppliers() { return supplierRepository.getAllSuppliers(0, 200, null, "supCompany"); } + /** + * Saves or updates the product-supplier relationship. + */ public LiveData> saveProductSupplier(ProductSupplierDTO dto) { if (isEditing) { return psRepository.updateProductSupplier(editProductId, editSupplierId, dto); @@ -70,13 +96,30 @@ public class ProductSupplierDetailViewModel extends ViewModel { } } + /** + * Deletes the current product-supplier relationship. + */ public LiveData> deleteProductSupplier() { return psRepository.deleteProductSupplier(editProductId, editSupplierId); } + /** + * Updates the product list. + */ public void setProductList(List list) { productList.setValue(list); } + + /** + * Returns the LiveData for the product list. + */ public LiveData> getProductList() { return productList; } + /** + * Updates the supplier list. + */ public void setSupplierList(List list) { supplierList.setValue(list); } + + /** + * Returns the LiveData for the supplier list. + */ public LiveData> getSupplierList() { return supplierList; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java index 930770a4..0a568e51 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierListViewModel.java @@ -44,12 +44,34 @@ public class ProductSupplierListViewModel extends ViewModel { this.supplierRepository = supplierRepository; } + /** + * Returns the LiveData for the product-supplier relationship list. + */ public LiveData> getProductSuppliers() { return productSuppliers; } + + /** + * Returns the LiveData for the list of products for filtering. + */ public LiveData> getProducts() { return products; } + + /** + * Returns the LiveData for the list of suppliers for filtering. + */ public LiveData> getSuppliers() { return suppliers; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of product-supplier relationships has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads product-supplier relationships from the repository with the specified filters. + */ public void loadProductSuppliers(boolean reset, String query, Long productId, Long supplierId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -77,6 +99,9 @@ public class ProductSupplierListViewModel extends ViewModel { }); } + /** + * Loads products and suppliers for filtering options. + */ public void loadFilterData() { observeOnce(productRepository.getAllProducts(null, null, 0, 100, "prodName"), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -103,6 +128,9 @@ public class ProductSupplierListViewModel extends ViewModel { }); } + /** + * Deletes multiple product-supplier relationships by their composite keys. + */ public LiveData> bulkDeleteProductSuppliers(List ids) { return psRepository.bulkDeleteProductSuppliers(new BulkDeleteRequest(ids)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java index 436cfa4c..c68e1d89 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderDetailViewModel.java @@ -20,6 +20,9 @@ public class PurchaseOrderDetailViewModel extends ViewModel { this.repository = repository; } + /** + * Loads the purchase order details from the repository. + */ public LiveData> loadPurchaseOrder(long id) { return repository.getPurchaseOrderById(id); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java index 923d336e..3febfe0e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderListViewModel.java @@ -38,11 +38,29 @@ public class PurchaseOrderListViewModel extends ViewModel { this.storeRepository = storeRepository; } + /** + * Returns the LiveData for the list of purchase orders. + */ public LiveData> getPurchaseOrders() { return purchaseOrders; } + + /** + * Returns the LiveData for the list of stores for filtering. + */ public LiveData> getStores() { return stores; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of purchase orders has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads purchase orders from the repository with the specified filters. + */ public void loadPurchaseOrders(boolean reset, String query, Long storeId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -70,6 +88,9 @@ public class PurchaseOrderListViewModel extends ViewModel { }); } + /** + * Loads the list of stores for filtering options. + */ public void loadStores() { observeOnce(storeRepository.getAllStores(0, 100), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java index ff3d069e..bf96e89b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/RefundViewModel.java @@ -20,6 +20,9 @@ import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; +/** + * ViewModel for processing and managing refund requests. + */ @HiltViewModel public class RefundViewModel extends ViewModel { private final SaleRepository saleRepository; @@ -34,35 +37,59 @@ public class RefundViewModel extends ViewModel { this.saleRepository = saleRepository; } + /** + * Loads all sales from the repository for refund selection. + */ public LiveData>> loadAllSales() { return saleRepository.getAllSales(0, 1000, null, null, null, null, null, "saleDate,desc"); } + /** + * Sets the list of all available sales. + */ public void setAllSales(List sales) { allSales.setValue(sales); } + /** + * Returns the list of all available sales. + */ public List getAllSalesList() { return allSales.getValue(); } + /** + * Sets the current sale being processed for a refund and computes refundable items. + */ public void setCurrentSale(SaleDTO sale) { currentSale.setValue(sale); buildRefundableItems(); } + /** + * Returns the current sale being processed. + */ public SaleDTO getCurrentSale() { return currentSale.getValue(); } + /** + * Returns the LiveData for the list of items available for refund in the current sale. + */ public LiveData> getAvailableItems() { return availableItems; } + /** + * Returns the LiveData for the items added to the refund cart. + */ public LiveData> getRefundCart() { return refundCart; } + /** + * Builds the list of items available for refund, accounting for previous refunds. + */ private void buildRefundableItems() { SaleDTO sale = currentSale.getValue(); List sales = allSales.getValue(); @@ -103,6 +130,9 @@ public class RefundViewModel extends ViewModel { refundCart.setValue(new ArrayList<>()); } + /** + * Adds an item to the refund cart. + */ public void addToCart(RefundItem item, int qty) { List cart = new ArrayList<>(refundCart.getValue()); boolean merged = false; @@ -120,12 +150,18 @@ public class RefundViewModel extends ViewModel { refundCart.setValue(cart); } + /** + * Removes an item from the refund cart. + */ public void removeFromCart(RefundItem item) { List cart = new ArrayList<>(refundCart.getValue()); cart.remove(item); refundCart.setValue(cart); } + /** + * Calculates the total refund amount, proportional to the original total. + */ public BigDecimal calculateRefundTotal() { SaleDTO sale = currentSale.getValue(); List cart = refundCart.getValue(); @@ -153,6 +189,9 @@ public class RefundViewModel extends ViewModel { return originalTotal.abs().multiply(ratio).setScale(2, RoundingMode.HALF_UP); } + /** + * Submits the refund transaction to the repository. + */ public LiveData> submitRefund(String paymentMethod) { SaleDTO sale = currentSale.getValue(); List cart = refundCart.getValue(); diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java index 68369cce..4160659e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleDetailViewModel.java @@ -24,6 +24,9 @@ import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; +/** + * ViewModel for managing sale details, including creating new sales and viewing existing ones. + */ @HiltViewModel public class SaleDetailViewModel extends ViewModel { private final SaleRepository saleRepository; @@ -55,77 +58,150 @@ public class SaleDetailViewModel extends ViewModel { this.couponRepository = couponRepository; } + /** + * Sets the sale ID and whether the view is read-only. + */ public void setSaleId(long id, boolean viewOnly) { this.saleId = id; this.viewOnly = viewOnly; } + /** + * Returns the current sale ID. + */ public long getSaleId() { return saleId; } + + /** + * Returns true if the view is in read-only mode. + */ public boolean isViewOnly() { return viewOnly; } + /** + * Loads the details of a specific sale by its ID. + */ public LiveData> loadSaleDetails() { return saleRepository.getSaleById(saleId); } + /** + * Loads the list of stores for selection. + */ public LiveData>> loadStores() { return storeRepository.getStoreDropdowns(); } + /** + * Loads the list of customers for selection. + */ public LiveData>> loadCustomers() { return customerRepository.getCustomerDropdowns(); } + /** + * Loads the list of products for sale selection. + */ public LiveData>> loadProducts() { return productRepository.getAllProducts(null, null, 0, 200, null); } + /** + * Submits a new sale to the repository. + */ public LiveData> createSale(SaleDTO sale) { return saleRepository.createSale(sale); } + /** + * Sets the local list of stores. + */ public void setStoreList(List list) { storeList.setValue(list); } + + /** + * Returns the LiveData for the local list of stores. + */ public LiveData> getStoreList() { return storeList; } + /** + * Sets the local list of customers. + */ public void setCustomerList(List list) { customerList.setValue(list); } + + /** + * Returns the LiveData for the local list of customers. + */ public LiveData> getCustomerList() { return customerList; } + /** + * Sets the local list of products. + */ public void setProductList(List list) { productList.setValue(list); } + + /** + * Returns the LiveData for the local list of products. + */ public LiveData> getProductList() { return productList; } + /** + * Adds an item to the sale cart. + */ public void addToCart(SaleDTO.SaleItemDTO item) { List currentCart = new ArrayList<>(cartItems.getValue()); currentCart.add(item); cartItems.setValue(currentCart); } + /** + * Removes an item from the sale cart by product ID. + */ public void removeFromCart(Long prodId) { List currentCart = new ArrayList<>(cartItems.getValue()); currentCart.removeIf(item -> item.getProdId().equals(prodId)); cartItems.setValue(currentCart); } + /** + * Returns the LiveData for the list of items in the cart. + */ public LiveData> getCartItems() { return cartItems; } + /** + * Looks up a coupon by its code. + */ public LiveData> lookupCoupon(String code) { return couponRepository.getCouponByCode(code); } + /** + * Sets the currently applied coupon. + */ public void setAppliedCoupon(CouponDTO coupon) { appliedCoupon.setValue(coupon); } + /** + * Sets whether to use loyalty points for the current sale. + */ public void setUseLoyaltyPoints(boolean use) { useLoyaltyPoints.setValue(use); } + /** + * Returns the LiveData for whether loyalty points are being used. + */ public LiveData getUseLoyaltyPoints() { return useLoyaltyPoints; } + /** + * Returns the LiveData for the selected customer's full data. + */ public LiveData getSelectedCustomerData() { return selectedCustomerData; } + /** + * Selects a customer and loads their full data. + */ public void selectCustomer(Long customerId) { if (customerId == null) { selectedCustomerData.setValue(null); @@ -142,23 +218,38 @@ public class SaleDetailViewModel extends ViewModel { }); } + /** + * Clears the currently applied coupon. + */ public void clearCoupon() { appliedCoupon.setValue(null); } + /** + * Returns the LiveData for the applied coupon. + */ public LiveData getAppliedCoupon() { return appliedCoupon; } + /** + * Returns the ID of the applied coupon. + */ public Long getAppliedCouponId() { CouponDTO coupon = appliedCoupon.getValue(); return coupon != null ? coupon.getCouponId() : null; } + /** + * Calculates the total discount (coupon + loyalty). + */ public BigDecimal calculateDiscount() { return calculateCouponDiscount().add(calculateLoyaltyDiscount()); } + /** + * Calculates the discount from the applied coupon. + */ public BigDecimal calculateCouponDiscount() { CouponDTO coupon = appliedCoupon.getValue(); if (coupon == null || coupon.getDiscountValue() == null) return BigDecimal.ZERO; @@ -170,6 +261,9 @@ public class SaleDetailViewModel extends ViewModel { } } + /** + * Calculates the discount from using loyalty points. + */ public BigDecimal calculateLoyaltyDiscount() { if (Boolean.FALSE.equals(useLoyaltyPoints.getValue())) return BigDecimal.ZERO; CustomerDTO customer = selectedCustomerData.getValue(); @@ -184,11 +278,17 @@ public class SaleDetailViewModel extends ViewModel { return BigDecimal.valueOf(pointsToUse).multiply(BigDecimal.valueOf(0.05)).setScale(2, java.math.RoundingMode.HALF_UP); } + /** + * Calculates the number of loyalty points to be used. + */ public int calculatePointsToUse() { BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(); return loyaltyDiscount.divide(BigDecimal.valueOf(0.05), 0, java.math.RoundingMode.HALF_UP).intValue(); } + /** + * Calculates the subtotal of all items in the cart. + */ public BigDecimal calculateSubtotal() { BigDecimal total = BigDecimal.ZERO; List items = cartItems.getValue(); @@ -206,10 +306,16 @@ public class SaleDetailViewModel extends ViewModel { return total; } + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + /** + * Sets the loading state. + */ public void setLoading(boolean loading) { isLoading.setValue(loading); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java index aee0f948..e3f4f7ee 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SaleListViewModel.java @@ -22,6 +22,9 @@ import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; +/** + * ViewModel for managing the list of sales transactions and applying filters. + */ @HiltViewModel public class SaleListViewModel extends ViewModel { private final SaleRepository saleRepository; @@ -44,12 +47,34 @@ public class SaleListViewModel extends ViewModel { this.customerRepository = customerRepository; } + /** + * Returns the LiveData for the list of sales. + */ public LiveData> getSales() { return sales; } + + /** + * Returns the LiveData for the list of available stores. + */ public LiveData> getStores() { return stores; } + + /** + * Returns the LiveData for the list of available customers. + */ public LiveData> getCustomers() { return customers; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Returns true if the current page is the last page of results. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads a page of sales based on search and filter criteria. + */ public void loadSales(boolean reset, String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -75,6 +100,9 @@ public class SaleListViewModel extends ViewModel { }); } + /** + * Loads available stores for filtering. + */ public void loadStores() { observeOnce(storeRepository.getAllStores(0, 100), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -83,6 +111,9 @@ public class SaleListViewModel extends ViewModel { }); } + /** + * Loads available customers for filtering. + */ public void loadCustomers() { observeOnce(customerRepository.getAllCustomers(0, 500), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -91,6 +122,9 @@ public class SaleListViewModel extends ViewModel { }); } + /** + * Observes a LiveData once, removing the observer after the first non-loading response. + */ private void observeOnce(LiveData> liveData, Observer> handler) { liveData.observeForever(new Observer>() { @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java index 625abb92..352a08b7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceDetailViewModel.java @@ -26,24 +26,39 @@ public class ServiceDetailViewModel extends ViewModel { this.repository = repository; } + /** + * Sets the service ID and initializes the editing mode. + */ public void setServiceId(long id) { this.serviceId = id; initMode(id != -1); } + /** + * Returns the current service ID. + */ public long getServiceId() { return serviceId; } + /** + * Checks if the fragment is in editing mode. + */ public boolean isEditing() { ViewState current = viewState.getValue(); return current != null && current.isEditing; } + /** + * Returns the LiveData for the view state. + */ public LiveData getViewState() { return viewState; } + /** + * Initializes the UI mode (Create vs Edit). + */ public void initMode(boolean isEditing) { updateViewState(state -> { state.isEditing = isEditing; @@ -55,6 +70,9 @@ public class ServiceDetailViewModel extends ViewModel { }); } + /** + * Fetches service details from the repository. + */ public LiveData> loadService() { MutableLiveData> result = new MutableLiveData<>(); observeOnce(repository.getServiceById(serviceId), resource -> { @@ -72,6 +90,9 @@ public class ServiceDetailViewModel extends ViewModel { return result; } + /** + * Saves or updates the service record. + */ public LiveData> saveService(ServiceDTO dto) { updateViewState(state -> { state.serviceName = safeText(dto.getServiceName()); @@ -88,6 +109,9 @@ public class ServiceDetailViewModel extends ViewModel { return repository.createService(dto); } + /** + * Deletes the current service record. + */ public LiveData> deleteService() { return repository.deleteService(serviceId); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java index bbdbe270..7e3e9092 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceListViewModel.java @@ -35,10 +35,24 @@ public class ServiceListViewModel extends ViewModel { this.repository = repository; } + /** + * Returns the LiveData for the list of services. + */ public LiveData> getServices() { return services; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of services has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads services from the repository with the specified filters. + */ public void loadServices(boolean reset, String query) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -76,6 +90,9 @@ public class ServiceListViewModel extends ViewModel { }); } + /** + * Deletes multiple services by their IDs. + */ public LiveData> bulkDeleteServices(List ids) { return repository.bulkDeleteServices(new BulkDeleteRequest(ids)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java index 7927465e..8dd538a9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffDetailViewModel.java @@ -31,31 +31,52 @@ public class StaffDetailViewModel extends ViewModel { this.storeRepository = storeRepository; } + /** + * Loads store dropdown options. + */ public LiveData>> loadStores() { return storeRepository.getStoreDropdowns(); } + /** + * Returns the LiveData for the store list. + */ public LiveData> getStoreList() { return storeList; } + /** + * Updates the store list. + */ public void setStoreList(List list) { storeList.setValue(list); } + /** + * Loads the employee details from the repository. + */ public LiveData> loadEmployee(long id) { return repository.getEmployeeById(id); } + /** + * Sets the current employee ID and editing mode. + */ public void setEmployeeId(long id, boolean isEditing) { this.employeeId = id; this.isEditing = isEditing; } + /** + * Returns the current employee ID. + */ public long getEmployeeId() { return employeeId; } + /** + * Checks if the fragment is in editing mode. + */ public boolean isEditing() { return isEditing; } @@ -68,6 +89,9 @@ public class StaffDetailViewModel extends ViewModel { return dto; } + /** + * Saves or updates the employee record. + */ public LiveData> saveEmployee(EmployeeDTO dto) { if (isEditing && employeeId > 0) { return repository.updateEmployee(employeeId, dto); @@ -76,6 +100,9 @@ public class StaffDetailViewModel extends ViewModel { } } + /** + * Deletes the current employee record. + */ public LiveData> deleteEmployee() { return repository.deleteEmployee(employeeId); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java index 3dc40816..ed22e3e5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StaffListViewModel.java @@ -19,6 +19,9 @@ import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; +/** + * ViewModel for managing the list of employees, including filtering and pagination. + */ @HiltViewModel public class StaffListViewModel extends ViewModel { private final EmployeeRepository repository; @@ -43,11 +46,29 @@ public class StaffListViewModel extends ViewModel { this.storeRepository = storeRepository; } + /** + * Returns the LiveData for the list of filtered employees. + */ public LiveData> getFilteredEmployees() { return filteredEmployees; } + + /** + * Returns the LiveData for the list of stores for filtering. + */ public LiveData> getStores() { return stores; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of employees has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads the full list of staff from the repository. + */ public void loadStaff(boolean reset) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -76,6 +97,9 @@ public class StaffListViewModel extends ViewModel { }); } + /** + * Loads the list of stores for filtering options. + */ public void loadStores() { observeOnce(storeRepository.getAllStores(0, 100), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -84,6 +108,9 @@ public class StaffListViewModel extends ViewModel { }); } + /** + * Observes a LiveData once + */ private void observeOnce(LiveData> liveData, Observer> handler) { liveData.observeForever(new Observer>() { @Override @@ -96,6 +123,9 @@ public class StaffListViewModel extends ViewModel { }); } + /** + * Filters the employee list locally based on query, store, and status. + */ public void filter(String query, Long storeId, String status) { this.lastQuery = query; this.lastStoreId = storeId; @@ -103,6 +133,9 @@ public class StaffListViewModel extends ViewModel { applyFilter(employees.getValue()); } + /** + * Applies the filters to the full list and updates the filtered LiveData. + */ private void applyFilter(List all) { if (all == null) return; diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java index 54f83ef9..63e03b7d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierDetailViewModel.java @@ -26,24 +26,39 @@ public class SupplierDetailViewModel extends ViewModel { this.repository = repository; } + /** + * Sets the supplier ID and initializes the editing mode. + */ public void setSupId(long id) { this.supId = id; initMode(id != -1); } + /** + * Returns the current supplier ID. + */ public long getSupId() { return supId; } + /** + * Checks if the fragment is in editing mode. + */ public boolean isEditing() { ViewState current = viewState.getValue(); return current != null && current.isEditing; } + /** + * Returns the LiveData for the view state. + */ public LiveData getViewState() { return viewState; } + /** + * Initializes the UI mode (Create vs Edit). + */ public void initMode(boolean isEditing) { updateViewState(state -> { state.isEditing = isEditing; @@ -55,6 +70,9 @@ public class SupplierDetailViewModel extends ViewModel { }); } + /** + * Fetches supplier details from the repository. + */ public LiveData> loadSupplier() { MutableLiveData> result = new MutableLiveData<>(); observeOnce(repository.getSupplierById(supId), resource -> { @@ -73,6 +91,9 @@ public class SupplierDetailViewModel extends ViewModel { return result; } + /** + * Saves or updates the supplier record. + */ public LiveData> saveSupplier(SupplierDTO dto) { if (isEditing()) { dto.setSupId(supId); @@ -81,6 +102,9 @@ public class SupplierDetailViewModel extends ViewModel { return repository.createSupplier(dto); } + /** + * Deletes the current supplier record. + */ public LiveData> deleteSupplier() { return repository.deleteSupplier(supId); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java index 33373e90..700977cf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierListViewModel.java @@ -34,10 +34,24 @@ public class SupplierListViewModel extends ViewModel { this.repository = repository; } + /** + * Returns the LiveData for the list of suppliers. + */ public LiveData> getSuppliers() { return suppliers; } + + /** + * Returns the LiveData for the loading state. + */ public LiveData getIsLoading() { return isLoading; } + + /** + * Checks if the last page of suppliers has been reached. + */ public boolean isLastPage() { return isLastPage; } + /** + * Loads suppliers from the repository with the specified filters. + */ public void loadSuppliers(boolean reset, String query) { if (isLoading.getValue() != null && isLoading.getValue() && !reset) return; @@ -77,6 +91,9 @@ public class SupplierListViewModel extends ViewModel { }); } + /** + * Deletes multiple suppliers by their IDs. + */ public LiveData> bulkDeleteSuppliers(List ids) { return repository.bulkDeleteSuppliers(new BulkDeleteRequest(ids)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java b/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java index 3aad4b8d..8d77daef 100644 --- a/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java +++ b/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java @@ -18,25 +18,50 @@ import ua.naiksoftware.stomp.Stomp; import ua.naiksoftware.stomp.StompClient; import ua.naiksoftware.stomp.dto.StompHeader; -//Used to handle the websocket connection for the chat +/** + * Manages WebSocket connections and STOMP protocol messaging for the chat system. + */ public class StompChatManager { private static final String TAG = "StompChatManager"; - //Interface for when a message is received + /** + * Interface for receiving new chat messages. + */ public interface MessageListener { + /** + * Called when a new message is received for the current conversation. + */ void onMessageReceived(MessageDTO message); } - //Interface for when a conversation is created or updated + /** + * Interface for receiving updates to the list of conversations. + */ public interface ConversationListener { + /** + * Called when a conversation is created or its details are updated. + */ void onConversationUpdated(ConversationDTO conversation); } - //Interface for when the websocket connection is opened, closed, or has an error + /** + * Interface for monitoring the status of the WebSocket connection. + */ public interface ConnectionListener { + /** + * Called when the connection is successfully established. + */ void onSocketOpened(); + + /** + * Called when the connection is closed. + */ void onSocketClosed(); + + /** + * Called when a connection error occurs. + */ void onSocketError(); } @@ -59,16 +84,25 @@ public class StompChatManager { private boolean manualDisconnect; private Long pendingConversationId; + /** + * Initializes the manager with authentication and server details. + */ public StompChatManager(String authToken, String role, String baseUrl) { this.authToken = authToken; this.role = role == null ? "" : role.trim().toUpperCase(Locale.ROOT); this.baseUrl = baseUrl; } + /** + * Sets the listener for incoming messages. + */ public void setMessageListener(MessageListener listener) { this.messageListener = listener; } + /** + * Sets the listener for conversation list updates. + */ public void setConversationListener(ConversationListener listener) { this.conversationListener = listener; } @@ -77,7 +111,10 @@ public class StompChatManager { this.connectionListener = listener; } - // Set up a stomp connection + /** + * Establishes a connection to the WebSocket server using STOMP. + * Reconnects automatically unless manually disconnected. + */ public void connect() { if (authToken == null || authToken.isBlank()) { Log.e(TAG, "Cannot connect websocket without token"); @@ -143,7 +180,10 @@ public class StompChatManager { )); } - // Subscribes to updates for a specific conversation + /** + * Subscribes to updates for a specific conversation. + * If not connected, the subscription is queued until the connection is established. + */ public void subscribeToConversation(Long conversationId) { pendingConversationId = conversationId; if (!isConnected || stompClient == null) { @@ -154,7 +194,9 @@ public class StompChatManager { subscribeToTopic(conversationId); } - // Clears the current conversation subscription + /** + * Stops listening for messages in the current conversation. + */ public void clearConversationSubscription() { pendingConversationId = null; if (topicDisposable != null && !topicDisposable.isDisposed()) { @@ -163,7 +205,9 @@ public class StompChatManager { } } - //helper function to subscribe to a specific conversation topic + /** + * Subscribes to the specific STOMP topic for a conversation's messages. + */ private void subscribeToTopic(Long conversationId) { if (topicDisposable != null && !topicDisposable.isDisposed()) { topicDisposable.dispose(); @@ -189,7 +233,9 @@ public class StompChatManager { compositeDisposable.add(topicDisposable); } - // Listens for conversation updates and refresh the chat list + /** + * Listens for conversation updates and refresh the chat list + */ private void subscribeToConversationFeeds() { if (conversationsDisposable != null && !conversationsDisposable.isDisposed()) { conversationsDisposable.dispose(); @@ -254,7 +300,9 @@ public class StompChatManager { compositeDisposable.add(errorQueueDisposable); } - // Disconnects the stomp connection + /** + * Disconnects the stomp connection + */ public void disconnect() { manualDisconnect = true; isConnected = false; @@ -267,7 +315,9 @@ public class StompChatManager { } } - // Make the URL for the websocket connection + /** + * Make the URL for the websocket connection + */ private String buildWebSocketUrl() { String cleanBaseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) @@ -281,12 +331,16 @@ public class StompChatManager { return cleanBaseUrl + "/ws/chat"; } - // Helper to check if the current user is a customer + /** + * Helper to check if the current user is a customer + */ private boolean isCustomer() { return "CUSTOMER".equals(role); } - // if connection drops, try to reconnect after 1 second + /** + * if connection drops, try to reconnect after 1 second + */ private void scheduleReconnect() { if (manualDisconnect) { return; -- 2.49.1 From c10c9d6d781f4af9673af4101f1520200fbc76c1 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 05:09:57 -0600 Subject: [PATCH 15/34] fix HQL pet query --- .../com/petshop/backend/repository/AppointmentRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index 59243df2..b23b1902 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -53,7 +53,7 @@ public interface AppointmentRepository extends JpaRepository List findByPet_Id(Long petId); - @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.pet.petId = :petId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.pet.id = :petId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByPetIdAndAppointmentDate(@Param("petId") Long petId, @Param("date") LocalDate date); List findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status); -- 2.49.1 From 9d70665b60c6311dc468dad389c00acf05a665ac Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 05:36:52 -0600 Subject: [PATCH 16/34] clean flyway config --- backend/src/main/resources/application.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 21d7f6d4..20773f86 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -40,7 +40,6 @@ spring: flyway: enabled: ${FLYWAY_ENABLED:false} - validate-on-migrate: false server: port: ${SERVER_PORT:8080} -- 2.49.1 From 1523aef51eed052c488686fe9721da5dc5054220 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Mon, 20 Apr 2026 05:41:36 -0600 Subject: [PATCH 17/34] Minor tweaks, changed checkout UI --- web/app/adopt/page.js | 2 +- web/app/products/page.js | 2 +- web/components/ProductProfile.js | 17 +++++++++-------- web/public/bootstrap/cart-plus-fill.svg | 3 +++ 4 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 web/public/bootstrap/cart-plus-fill.svg diff --git a/web/app/adopt/page.js b/web/app/adopt/page.js index f32eab3a..7cc23723 100644 --- a/web/app/adopt/page.js +++ b/web/app/adopt/page.js @@ -131,7 +131,7 @@ export default function AdoptPage() { setSearch(e.target.value)} /> diff --git a/web/app/products/page.js b/web/app/products/page.js index 48959a2a..155ba778 100644 --- a/web/app/products/page.js +++ b/web/app/products/page.js @@ -56,7 +56,7 @@ export default function ProductsPage() { setSearch(e.target.value)} /> diff --git a/web/components/ProductProfile.js b/web/components/ProductProfile.js index efd2ece7..98cb356c 100644 --- a/web/components/ProductProfile.js +++ b/web/components/ProductProfile.js @@ -26,6 +26,7 @@ export default function ProductProfile({ prodId, prodName, categoryName, prodDes await addItem(prodId, quantity); setFeedback({ type: "success", message: `${quantity} × ${prodName} added to cart!` }); setQuantity(1); + setTimeout(() => setFeedback(null), 1000); } catch (err) { setFeedback({ type: "error", message: err.message ?? "Failed to add to cart." }); } finally { @@ -90,20 +91,20 @@ export default function ProductProfile({ prodId, prodName, categoryName, prodDes
-
- {feedback && ( -

- {feedback.message} -

- )} -
+ {feedback?.type === "error" && ( +

+ {feedback.message} +

+ )} )}
diff --git a/web/public/bootstrap/cart-plus-fill.svg b/web/public/bootstrap/cart-plus-fill.svg new file mode 100644 index 00000000..59e46e48 --- /dev/null +++ b/web/public/bootstrap/cart-plus-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file -- 2.49.1 From b223b214710b69a0d673d89aca7b7ee47b32c6f8 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 05:45:10 -0600 Subject: [PATCH 18/34] add flyway baseline config --- backend/src/main/resources/application.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 20773f86..89ad051f 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -40,6 +40,8 @@ spring: flyway: enabled: ${FLYWAY_ENABLED:false} + baseline-on-migrate: true + baseline-version: 0 server: port: ${SERVER_PORT:8080} -- 2.49.1 From bfaa1b0f7ba04158e096b0cf5156fa97acf0fe3c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 05:53:49 -0600 Subject: [PATCH 19/34] fix flyway baseline config --- .../com/petshop/backend/config/FlywayContextInitializer.java | 3 ++- backend/src/main/resources/application.yml | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java b/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java index d7846581..15c9b976 100644 --- a/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java +++ b/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java @@ -37,7 +37,8 @@ public class FlywayContextInitializer implements ApplicationContextInitializer Date: Mon, 20 Apr 2026 06:02:05 -0600 Subject: [PATCH 20/34] idempotent schema indexes --- .../db/migration/V1__target_baseline.sql | 83 +++++++++---------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/backend/src/main/resources/db/migration/V1__target_baseline.sql b/backend/src/main/resources/db/migration/V1__target_baseline.sql index 6f03cf58..41210e0a 100644 --- a/backend/src/main/resources/db/migration/V1__target_baseline.sql +++ b/backend/src/main/resources/db/migration/V1__target_baseline.sql @@ -27,7 +27,10 @@ CREATE TABLE IF NOT EXISTS users ( tokenVersion INT NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT fk_users_primary_store FOREIGN KEY (primaryStoreId) REFERENCES storeLocation(storeId) ON DELETE SET NULL + CONSTRAINT fk_users_primary_store FOREIGN KEY (primaryStoreId) REFERENCES storeLocation(storeId) ON DELETE SET NULL, + INDEX idx_users_primary_store (primaryStoreId), + INDEX idx_users_role (role), + INDEX idx_users_name (lastName, firstName) ); CREATE TABLE IF NOT EXISTS supplier ( @@ -65,7 +68,8 @@ CREATE TABLE IF NOT EXISTS service_species ( species VARCHAR(50) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (serviceId, species), - CONSTRAINT fk_service_species_service FOREIGN KEY (serviceId) REFERENCES service(serviceId) ON DELETE CASCADE + CONSTRAINT fk_service_species_service FOREIGN KEY (serviceId) REFERENCES service(serviceId) ON DELETE CASCADE, + INDEX idx_service_species_species (species) ); CREATE TABLE IF NOT EXISTS product ( @@ -89,7 +93,9 @@ CREATE TABLE IF NOT EXISTS inventory ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT uq_inventory_store_product UNIQUE (storeId, prodId), CONSTRAINT fk_inventory_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), - CONSTRAINT fk_inventory_product FOREIGN KEY (prodId) REFERENCES product(prodId) + CONSTRAINT fk_inventory_product FOREIGN KEY (prodId) REFERENCES product(prodId), + INDEX idx_inventory_store (storeId), + INDEX idx_inventory_product (prodId) ); CREATE TABLE IF NOT EXISTS productSupplier ( @@ -111,7 +117,8 @@ CREATE TABLE IF NOT EXISTS purchaseOrder ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_purchase_order_supplier FOREIGN KEY (supId) REFERENCES supplier(supId), - CONSTRAINT fk_purchase_order_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) + CONSTRAINT fk_purchase_order_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), + INDEX idx_purchase_order_store (storeId) ); CREATE TABLE IF NOT EXISTS coupon ( @@ -143,7 +150,11 @@ CREATE TABLE IF NOT EXISTS pet ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_pet_owner_user FOREIGN KEY (ownerUserId) REFERENCES users(id) ON DELETE SET NULL, - CONSTRAINT fk_pet_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) ON DELETE SET NULL + CONSTRAINT fk_pet_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) ON DELETE SET NULL, + INDEX idx_pet_owner_user (ownerUserId), + INDEX idx_pet_store (storeId), + INDEX idx_pet_species (petSpecies), + INDEX idx_pet_name (petName) ); CREATE TABLE IF NOT EXISTS appointment ( @@ -162,7 +173,12 @@ CREATE TABLE IF NOT EXISTS appointment ( CONSTRAINT fk_appointment_pet FOREIGN KEY (petId) REFERENCES pet(petId) ON DELETE SET NULL, CONSTRAINT fk_appointment_customer FOREIGN KEY (customerId) REFERENCES users(id), CONSTRAINT fk_appointment_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), - CONSTRAINT fk_appointment_employee FOREIGN KEY (employeeId) REFERENCES users(id) + CONSTRAINT fk_appointment_employee FOREIGN KEY (employeeId) REFERENCES users(id), + INDEX idx_appointment_store (storeId), + INDEX idx_appointment_employee (employeeId), + INDEX idx_appointment_customer (customerId), + INDEX idx_appointment_pet (petId), + INDEX idx_appointment_date_status (appointmentDate, appointmentStatus) ); CREATE TABLE IF NOT EXISTS adoption ( @@ -178,7 +194,9 @@ CREATE TABLE IF NOT EXISTS adoption ( CONSTRAINT fk_adoption_pet FOREIGN KEY (petId) REFERENCES pet(petId), CONSTRAINT fk_adoption_customer FOREIGN KEY (customerId) REFERENCES users(id), CONSTRAINT fk_adoption_employee FOREIGN KEY (employeeId) REFERENCES users(id), - CONSTRAINT fk_adoption_source_store FOREIGN KEY (sourceStoreId) REFERENCES storeLocation(storeId) + CONSTRAINT fk_adoption_source_store FOREIGN KEY (sourceStoreId) REFERENCES storeLocation(storeId), + INDEX idx_adoption_store (sourceStoreId), + INDEX idx_adoption_employee (employeeId) ); CREATE TABLE IF NOT EXISTS cart ( @@ -200,7 +218,8 @@ CREATE TABLE IF NOT EXISTS cart ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_cart_user FOREIGN KEY (userId) REFERENCES users(id), CONSTRAINT fk_cart_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) ON DELETE SET NULL, - CONSTRAINT fk_cart_coupon FOREIGN KEY (couponId) REFERENCES coupon(couponId) ON DELETE SET NULL + CONSTRAINT fk_cart_coupon FOREIGN KEY (couponId) REFERENCES coupon(couponId) ON DELETE SET NULL, + INDEX idx_cart_user (userId) ); CREATE TABLE IF NOT EXISTS cart_item ( @@ -243,7 +262,11 @@ CREATE TABLE IF NOT EXISTS sale ( CONSTRAINT fk_sale_customer FOREIGN KEY (customerId) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT fk_sale_original_sale FOREIGN KEY (originalSaleId) REFERENCES sale(saleId), CONSTRAINT fk_sale_cart FOREIGN KEY (cartId) REFERENCES cart(cartId) ON DELETE SET NULL, - CONSTRAINT fk_sale_coupon FOREIGN KEY (couponId) REFERENCES coupon(couponId) ON DELETE SET NULL + CONSTRAINT fk_sale_coupon FOREIGN KEY (couponId) REFERENCES coupon(couponId) ON DELETE SET NULL, + INDEX idx_sale_store (storeId), + INDEX idx_sale_employee (employeeId), + INDEX idx_sale_customer (customerId), + INDEX idx_sale_date (saleDate) ); CREATE TABLE IF NOT EXISTS saleItem ( @@ -291,12 +314,11 @@ CREATE TABLE IF NOT EXISTS passwordResetToken ( usedAt DATETIME NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_password_reset_token_hash UNIQUE (tokenHash), - CONSTRAINT fk_password_reset_token_user FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE + CONSTRAINT fk_password_reset_token_user FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_password_reset_token_user (userId), + INDEX idx_password_reset_token_expires (expiresAt) ); -CREATE INDEX idx_password_reset_token_user ON passwordResetToken(userId); -CREATE INDEX idx_password_reset_token_expires ON passwordResetToken(expiresAt); - CREATE TABLE IF NOT EXISTS conversation ( id BIGINT AUTO_INCREMENT PRIMARY KEY, customerId BIGINT NOT NULL, @@ -307,7 +329,9 @@ CREATE TABLE IF NOT EXISTS conversation ( created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_conversation_customer FOREIGN KEY (customerId) REFERENCES users(id), - CONSTRAINT fk_conversation_staff FOREIGN KEY (staffId) REFERENCES users(id) ON DELETE SET NULL + CONSTRAINT fk_conversation_staff FOREIGN KEY (staffId) REFERENCES users(id) ON DELETE SET NULL, + INDEX idx_conversation_customer (customerId), + INDEX idx_conversation_staff (staffId) ); CREATE TABLE IF NOT EXISTS message ( @@ -336,33 +360,8 @@ CREATE TABLE IF NOT EXISTS activityLog ( activity TEXT NOT NULL, logTimestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_activity_log_user FOREIGN KEY (userId) REFERENCES users(id), - CONSTRAINT fk_activity_log_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) ON DELETE SET NULL + CONSTRAINT fk_activity_log_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) ON DELETE SET NULL, + INDEX idx_activity_log_store (storeId), + INDEX idx_activity_log_timestamp_id (logTimestamp, logId) ); -CREATE INDEX idx_users_primary_store ON users(primaryStoreId); -CREATE INDEX idx_users_role ON users(role); -CREATE INDEX idx_users_name ON users(lastName, firstName); -CREATE INDEX idx_service_species_species ON service_species(species); -CREATE INDEX idx_inventory_store ON inventory(storeId); -CREATE INDEX idx_inventory_product ON inventory(prodId); -CREATE INDEX idx_purchase_order_store ON purchaseOrder(storeId); -CREATE INDEX idx_pet_owner_user ON pet(ownerUserId); -CREATE INDEX idx_pet_store ON pet(storeId); -CREATE INDEX idx_pet_species ON pet(petSpecies); -CREATE INDEX idx_pet_name ON pet(petName); -CREATE INDEX idx_appointment_store ON appointment(storeId); -CREATE INDEX idx_appointment_employee ON appointment(employeeId); -CREATE INDEX idx_appointment_customer ON appointment(customerId); -CREATE INDEX idx_appointment_pet ON appointment(petId); -CREATE INDEX idx_appointment_date_status ON appointment(appointmentDate, appointmentStatus); -CREATE INDEX idx_adoption_store ON adoption(sourceStoreId); -CREATE INDEX idx_adoption_employee ON adoption(employeeId); -CREATE INDEX idx_sale_store ON sale(storeId); -CREATE INDEX idx_sale_employee ON sale(employeeId); -CREATE INDEX idx_sale_customer ON sale(customerId); -CREATE INDEX idx_sale_date ON sale(saleDate); -CREATE INDEX idx_cart_user ON cart(userId); -CREATE INDEX idx_conversation_customer ON conversation(customerId); -CREATE INDEX idx_conversation_staff ON conversation(staffId); -CREATE INDEX idx_activity_log_store ON activityLog(storeId); -CREATE INDEX idx_activity_log_timestamp_id ON activityLog(logTimestamp, logId); -- 2.49.1 From fc3f1eb6bea052f6c7816db2f79b24374c9c564b Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 06:16:56 -0600 Subject: [PATCH 21/34] drop status from seed data --- .../resources/db/migration/V2__seed_data.sql | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql index 33505a84..4109ec6a 100644 --- a/backend/src/main/resources/db/migration/V2__seed_data.sql +++ b/backend/src/main/resources/db/migration/V2__seed_data.sql @@ -791,43 +791,43 @@ INSERT INTO inventory (inventoryId, storeId, prodId, quantity) VALUES (299, 3, 99, 57), (300, 3, 100, 64); -INSERT INTO purchaseOrder (purchaseOrderId, supId, storeId, orderDate, status) VALUES -(1, 3, 1, '2026-01-06', 'RECEIVED'), -(2, 4, 1, '2026-01-13', 'RECEIVED'), -(3, 5, 1, '2026-01-20', 'RECEIVED'), -(4, 4, 2, '2026-01-07', 'RECEIVED'), -(5, 5, 2, '2026-01-14', 'RECEIVED'), -(6, 6, 2, '2026-01-21', 'RECEIVED'), -(7, 5, 3, '2026-01-08', 'RECEIVED'), -(8, 6, 3, '2026-01-15', 'RECEIVED'), -(9, 7, 3, '2026-01-22', 'RECEIVED'), -(10, 4, 1, '2026-02-06', 'RECEIVED'), -(11, 5, 1, '2026-02-13', 'RECEIVED'), -(12, 6, 1, '2026-02-20', 'RECEIVED'), -(13, 5, 2, '2026-02-07', 'RECEIVED'), -(14, 6, 2, '2026-02-14', 'RECEIVED'), -(15, 7, 2, '2026-02-21', 'RECEIVED'), -(16, 6, 3, '2026-02-08', 'RECEIVED'), -(17, 7, 3, '2026-02-15', 'RECEIVED'), -(18, 8, 3, '2026-02-22', 'RECEIVED'), -(19, 5, 1, '2026-03-06', 'RECEIVED'), -(20, 6, 1, '2026-03-13', 'RECEIVED'), -(21, 7, 1, '2026-03-20', 'RECEIVED'), -(22, 6, 2, '2026-03-07', 'RECEIVED'), -(23, 7, 2, '2026-03-14', 'RECEIVED'), -(24, 8, 2, '2026-03-21', 'RECEIVED'), -(25, 7, 3, '2026-03-08', 'RECEIVED'), -(26, 8, 3, '2026-03-15', 'RECEIVED'), -(27, 9, 3, '2026-03-22', 'RECEIVED'), -(28, 6, 1, '2026-04-06', 'PENDING'), -(29, 7, 1, '2026-04-13', 'RECEIVED'), -(30, 8, 1, '2026-04-20', 'PLACED'), -(31, 7, 2, '2026-04-07', 'RECEIVED'), -(32, 8, 2, '2026-04-14', 'PLACED'), -(33, 9, 2, '2026-04-21', 'PENDING'), -(34, 8, 3, '2026-04-08', 'PLACED'), -(35, 9, 3, '2026-04-15', 'PENDING'), -(36, 10, 3, '2026-04-22', 'RECEIVED'); +INSERT INTO purchaseOrder (purchaseOrderId, supId, storeId, orderDate) VALUES +(1, 3, 1, '2026-01-06'), +(2, 4, 1, '2026-01-13'), +(3, 5, 1, '2026-01-20'), +(4, 4, 2, '2026-01-07'), +(5, 5, 2, '2026-01-14'), +(6, 6, 2, '2026-01-21'), +(7, 5, 3, '2026-01-08'), +(8, 6, 3, '2026-01-15'), +(9, 7, 3, '2026-01-22'), +(10, 4, 1, '2026-02-06'), +(11, 5, 1, '2026-02-13'), +(12, 6, 1, '2026-02-20'), +(13, 5, 2, '2026-02-07'), +(14, 6, 2, '2026-02-14'), +(15, 7, 2, '2026-02-21'), +(16, 6, 3, '2026-02-08'), +(17, 7, 3, '2026-02-15'), +(18, 8, 3, '2026-02-22'), +(19, 5, 1, '2026-03-06'), +(20, 6, 1, '2026-03-13'), +(21, 7, 1, '2026-03-20'), +(22, 6, 2, '2026-03-07'), +(23, 7, 2, '2026-03-14'), +(24, 8, 2, '2026-03-21'), +(25, 7, 3, '2026-03-08'), +(26, 8, 3, '2026-03-15'), +(27, 9, 3, '2026-03-22'), +(28, 6, 1, '2026-04-06'), +(29, 7, 1, '2026-04-13'), +(30, 8, 1, '2026-04-20'), +(31, 7, 2, '2026-04-07'), +(32, 8, 2, '2026-04-14'), +(33, 9, 2, '2026-04-21'), +(34, 8, 3, '2026-04-08'), +(35, 9, 3, '2026-04-15'), +(36, 10, 3, '2026-04-22'); INSERT INTO coupon (couponId, couponCode, discountType, discountValue, minOrderAmount, active, startsAt, endsAt, usageLimit) VALUES (1, 'NOCODE', 'FIXED', 0.00, 0.00, 1, NULL, NULL, NULL), -- 2.49.1 From f5d90c1d4f8f9bb22c9c149b02e1a2ac5258eded Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 06:28:08 -0600 Subject: [PATCH 22/34] fix store imageUrl length --- backend/src/main/resources/db/migration/V1__target_baseline.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/db/migration/V1__target_baseline.sql b/backend/src/main/resources/db/migration/V1__target_baseline.sql index 41210e0a..01a0c34e 100644 --- a/backend/src/main/resources/db/migration/V1__target_baseline.sql +++ b/backend/src/main/resources/db/migration/V1__target_baseline.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS storeLocation ( address VARCHAR(255) NOT NULL, phone VARCHAR(20) NOT NULL, email VARCHAR(100) NOT NULL, - imageUrl VARCHAR(255) NULL, + imageUrl VARCHAR(500) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 2.49.1 From aa48d2428df092db72ca0be6b91a0571d296ea29 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 07:07:21 -0600 Subject: [PATCH 23/34] Add full QA script --- test-backend-full.sh | 725 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 725 insertions(+) create mode 100755 test-backend-full.sh diff --git a/test-backend-full.sh b/test-backend-full.sh new file mode 100755 index 00000000..ad8cbf75 --- /dev/null +++ b/test-backend-full.sh @@ -0,0 +1,725 @@ +#!/usr/bin/env bash +set -uo pipefail + +BASE="https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/api/v1" +PASS=0 FAIL=0 + +CLEANUP_USER_IDS=() +CLEANUP_PRODUCT_IDS=() +CLEANUP_CATEGORY_IDS=() +CLEANUP_PET_IDS=() +CLEANUP_MYPET_IDS=() +CLEANUP_SERVICE_IDS=() +CLEANUP_STORE_IDS=() +CLEANUP_APPT_IDS=() +CLEANUP_ADOPTION_IDS=() +CLEANUP_SALE_IDS=() +CLEANUP_COUPON_IDS=() +CLEANUP_SUPPLIER_IDS=() +CLEANUP_CONV_IDS=() + +check() { + local label="$1" expect="$2" method="$3" path="$4"; shift 4 + local code + code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -X "$method" "$BASE$path" "$@" 2>/dev/null) + if [ "$code" = "$expect" ]; then PASS=$((PASS+1)) + else FAIL=$((FAIL+1)); echo "FAIL: $label — expected $expect got $code"; fi +} + +check_field() { + local label="$1" expr="$2" expected="$3" + local actual + actual=$(jq -r "$expr" /tmp/qa_body.json 2>/dev/null) + if [ "$actual" = "$expected" ]; then PASS=$((PASS+1)) + else FAIL=$((FAIL+1)); echo "FAIL: $label — expected '$expected' got '$actual'"; fi +} + +check_field_contains() { + local label="$1" expr="$2" expected="$3" + local actual + actual=$(jq -r "$expr" /tmp/qa_body.json 2>/dev/null) + if echo "$actual" | grep -q "$expected" 2>/dev/null; then PASS=$((PASS+1)) + else FAIL=$((FAIL+1)); echo "FAIL: $label — expected contains '$expected' got '$actual'"; fi +} + +check_field_gt() { + local label="$1" expr="$2" threshold="$3" + local actual + actual=$(jq -r "$expr" /tmp/qa_body.json 2>/dev/null) + if [ "$(echo "$actual > $threshold" | bc 2>/dev/null)" = "1" ]; then PASS=$((PASS+1)) + else FAIL=$((FAIL+1)); echo "FAIL: $label — expected > $threshold got '$actual'"; fi +} + +id_from_body() { jq -r '.id // .userId // empty' /tmp/qa_body.json 2>/dev/null; } + +echo "=========================================" +echo " PET SHOP QA — Full Backend Test Suite" +echo "=========================================" + +echo "--- 1. AUTH ---" + +check "Admin login" 200 POST "/auth/login" -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' +ADMIN_TOKEN=$(jq -r '.token' /tmp/qa_body.json) +if [ "$ADMIN_TOKEN" = "null" ] || [ -z "$ADMIN_TOKEN" ]; then echo "FATAL: Admin login failed"; exit 1; fi + +check "Staff login" 200 POST "/auth/login" -H 'Content-Type: application/json' -d '{"username":"staff","password":"staff123"}' +STAFF_TOKEN=$(jq -r '.token' /tmp/qa_body.json) +if [ "$STAFF_TOKEN" = "null" ] || [ -z "$STAFF_TOKEN" ]; then echo "FATAL: Staff login failed"; exit 1; fi + +check "Customer login" 200 POST "/auth/login" -H 'Content-Type: application/json' -d '{"username":"customer","password":"customer123"}' +CUST_TOKEN=$(jq -r '.token' /tmp/qa_body.json) +if [ "$CUST_TOKEN" = "null" ] || [ -z "$CUST_TOKEN" ]; then echo "FATAL: Customer login failed"; exit 1; fi + +A=(-H "Authorization: Bearer $ADMIN_TOKEN") +S=(-H "Authorization: Bearer $STAFF_TOKEN") +C=(-H "Authorization: Bearer $CUST_TOKEN") +J=(-H "Content-Type: application/json") + +check "GET /auth/me admin" 200 GET "/auth/me" "${A[@]}" +check_field "Admin role" ".role" "ADMIN" +check "GET /auth/me staff" 200 GET "/auth/me" "${S[@]}" +check_field "Staff role" ".role" "STAFF" +check "GET /auth/me customer" 200 GET "/auth/me" "${C[@]}" +check_field "Customer role" ".role" "CUSTOMER" + +check "Admin avatar file" 200 GET "/auth/me/avatar/file" "${A[@]}" +check "Wrong password" 401 POST "/auth/login" "${J[@]}" -d '{"username":"admin","password":"wrong"}' +check "Empty login body" 400 POST "/auth/login" "${J[@]}" -d '{}' + +check "Register QA user" 201 POST "/auth/register" "${J[@]}" -d '{"username":"testuser_qa","password":"Test1234!","email":"qa@test.com","firstName":"QA","lastName":"Test","phone":"5551234567"}' +QA_USER_ID=$(id_from_body) +[ -n "$QA_USER_ID" ] && CLEANUP_USER_IDS+=("$QA_USER_ID") + +check "Login QA user" 200 POST "/auth/login" "${J[@]}" -d '{"username":"testuser_qa","password":"Test1234!"}' +QA_TOKEN=$(jq -r '.token' /tmp/qa_body.json) +check "GET /me QA user" 200 GET "/auth/me" -H "Authorization: Bearer $QA_TOKEN" +check_field "QA username" ".username" "testuser_qa" + +check "Duplicate register" 409 POST "/auth/register" "${J[@]}" -d '{"username":"admin","password":"Test1234!","email":"dup@test.com","firstName":"A","lastName":"B","phone":"5550000000"}' +if [ "$(jq -r '.status // empty' /tmp/qa_body.json 2>/dev/null)" != "" ]; then PASS=$((PASS)); else + check "Duplicate register alt" 400 POST "/auth/register" "${J[@]}" -d '{"username":"admin","password":"Test1234!","email":"dup2@test.com","firstName":"A","lastName":"B","phone":"5550000001"}' +fi +check "Register missing fields" 400 POST "/auth/register" "${J[@]}" -d '{"username":"incomplete_qa"}' + +echo "--- 2. PRODUCTS ---" + +check "GET /products" 200 GET "/products" +check "GET /products/1" 200 GET "/products/1" +check "GET /products?q=dog" 200 GET "/products?q=dog" +check "GET /products?categoryId=1" 200 GET "/products?categoryId=1" +check "GET /products/999999" 404 GET "/products/999999" + +check "Create product (admin)" 201 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_TEST_Product","description":"QA test","price":9.99,"categoryId":1}' +PROD_ID=$(id_from_body) +[ -n "$PROD_ID" ] && CLEANUP_PRODUCT_IDS+=("$PROD_ID") +check_field "Product name" ".prodName" "QA_TEST_Product" + +check "Create product (customer)" 403 POST "/products" "${C[@]}" "${J[@]}" -d '{"prodName":"QA_TEST_Nope","price":1,"categoryId":1}' +check "Create product (no auth)" 401 POST "/products" "${J[@]}" -d '{"prodName":"QA_TEST_Nope2","price":1,"categoryId":1}' +check "Create product missing name" 400 POST "/products" "${A[@]}" "${J[@]}" -d '{"price":1,"categoryId":1}' + +if [ -n "$PROD_ID" ]; then + check "Update product" 200 PUT "/products/$PROD_ID" "${A[@]}" "${J[@]}" -d "{\"prodName\":\"QA_TEST_Updated\",\"description\":\"updated\",\"price\":19.99,\"categoryId\":1}" + check_field "Updated name" ".prodName" "QA_TEST_Updated" +fi + +check "Create product to delete" 201 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_TEST_DeleteMe","description":"del","price":1,"categoryId":1}' +DEL_PROD_ID=$(id_from_body) +if [ -n "$DEL_PROD_ID" ]; then + check "Delete product" 200 DELETE "/products/$DEL_PROD_ID" "${A[@]}" + check "GET deleted product" 404 GET "/products/$DEL_PROD_ID" +fi + +check "GET /products paginated" 200 GET "/products?page=0&size=5" +check "GET /products sorted" 200 GET "/products?sort=price,desc" + +echo "--- 3. CATEGORIES ---" + +check "GET /categories" 200 GET "/categories" +check "GET /categories/1" 200 GET "/categories/1" +check "GET /categories/999999" 404 GET "/categories/999999" + +check "Create category (admin)" 201 POST "/categories" "${A[@]}" "${J[@]}" -d '{"catName":"QA_TEST_Category","description":"qa"}' +CAT_ID=$(id_from_body) +[ -n "$CAT_ID" ] && CLEANUP_CATEGORY_IDS+=("$CAT_ID") + +check "Create category (customer)" 403 POST "/categories" "${C[@]}" "${J[@]}" -d '{"catName":"QA_TEST_Nope"}' +check "Create category missing name" 400 POST "/categories" "${A[@]}" "${J[@]}" -d '{"description":"no name"}' + +if [ -n "$CAT_ID" ]; then + check "Update category" 200 PUT "/categories/$CAT_ID" "${A[@]}" "${J[@]}" -d "{\"catName\":\"QA_TEST_CatUpdated\",\"description\":\"updated\"}" + check_field "Updated cat name" ".catName" "QA_TEST_CatUpdated" +fi + +check "Create category to delete" 201 POST "/categories" "${A[@]}" "${J[@]}" -d '{"catName":"QA_TEST_CatDel","description":"del"}' +DEL_CAT_ID=$(id_from_body) +if [ -n "$DEL_CAT_ID" ]; then + check "Delete category" 200 DELETE "/categories/$DEL_CAT_ID" "${A[@]}" + check "GET deleted category" 404 GET "/categories/$DEL_CAT_ID" +fi + +echo "--- 4. PETS ---" + +check "GET /pets" 200 GET "/pets" +check "GET /pets/2" 200 GET "/pets/2" +check "GET /pets?species=Dog" 200 GET "/pets?species=Dog" +check "GET /pets?status=Available" 200 GET "/pets?status=Available" +check "GET /pets/999999" 404 GET "/pets/999999" +check "GET /my-pets (customer)" 200 GET "/my-pets" "${C[@]}" + +check "Admin create pet" 201 POST "/pets" "${A[@]}" "${J[@]}" -d '{"name":"QA_TEST_Pet","species":"Dog","breed":"Labrador","age":2,"gender":"Male","status":"Available","storeId":1,"price":100,"description":"QA test pet"}' +PET_ID=$(id_from_body) +[ -n "$PET_ID" ] && CLEANUP_PET_IDS+=("$PET_ID") +check_field "Pet name" ".name" "QA_TEST_Pet" +check_field "Pet species" ".species" "Dog" + +if [ -n "$PET_ID" ]; then + check "Update pet" 200 PUT "/pets/$PET_ID" "${A[@]}" "${J[@]}" -d "{\"name\":\"QA_TEST_PetUpd\",\"species\":\"Dog\",\"breed\":\"Labrador\",\"age\":3,\"gender\":\"Male\",\"status\":\"Available\",\"storeId\":1,\"price\":150,\"description\":\"updated\"}" + check_field "Updated pet name" ".name" "QA_TEST_PetUpd" +fi + +check "Admin create pet to delete" 201 POST "/pets" "${A[@]}" "${J[@]}" -d '{"name":"QA_TEST_PetDel","species":"Cat","breed":"Siamese","age":1,"gender":"Female","status":"Available","storeId":1,"price":50,"description":"del"}' +DEL_PET_ID=$(id_from_body) +if [ -n "$DEL_PET_ID" ]; then + check "Delete pet" 200 DELETE "/pets/$DEL_PET_ID" "${A[@]}" + check "GET deleted pet" 404 GET "/pets/$DEL_PET_ID" +fi + +check "Customer create my-pet" 201 POST "/my-pets" "${C[@]}" "${J[@]}" -d '{"name":"QA_TEST_MyPet","species":"Rabbit","breed":"Holland Lop","age":1,"gender":"Female","description":"my pet"}' +MY_PET_ID=$(id_from_body) +[ -n "$MY_PET_ID" ] && CLEANUP_MYPET_IDS+=("$MY_PET_ID") +check "GET /my-pets has new pet" 200 GET "/my-pets" "${C[@]}" + +check "Create pet (customer direct)" 403 POST "/pets" "${C[@]}" "${J[@]}" -d '{"name":"QA_TEST_Nope","species":"Dog","breed":"Lab","age":1,"gender":"Male","status":"Available","storeId":1,"price":10}' +check "Create pet (no auth)" 401 POST "/pets" "${J[@]}" -d '{"name":"QA_TEST_Nope2","species":"Dog","breed":"Lab","age":1,"gender":"Male","status":"Available","storeId":1,"price":10}' +check "GET /pets paginated" 200 GET "/pets?page=0&size=5" + +echo "--- 5. SERVICES ---" + +check "GET /services" 200 GET "/services" +check "GET /services/1" 200 GET "/services/1" +check "GET /services?species=Dog" 200 GET "/services?species=Dog" +check "GET /services/999999" 404 GET "/services/999999" + +check "Create service (admin)" 201 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_TEST_Service","description":"qa svc","basePrice":25.00,"duration":30,"species":["Dog","Cat"]}' +SVC_ID=$(id_from_body) +[ -n "$SVC_ID" ] && CLEANUP_SERVICE_IDS+=("$SVC_ID") +check_field "Service name" ".serviceName" "QA_TEST_Service" + +if [ -n "$SVC_ID" ]; then + check "Update service" 200 PUT "/services/$SVC_ID" "${A[@]}" "${J[@]}" -d "{\"serviceName\":\"QA_TEST_SvcUpd\",\"description\":\"upd\",\"basePrice\":30,\"duration\":45,\"species\":[\"Dog\"]}" +fi + +check "Create service to delete" 201 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_TEST_SvcDel","description":"del","basePrice":10,"duration":15,"species":["Cat"]}' +DEL_SVC_ID=$(id_from_body) +if [ -n "$DEL_SVC_ID" ]; then + check "Delete service" 200 DELETE "/services/$DEL_SVC_ID" "${A[@]}" +fi +check "Create service (customer)" 403 POST "/services" "${C[@]}" "${J[@]}" -d '{"serviceName":"QA_TEST_Nope","basePrice":10,"duration":10,"species":["Dog"]}' + +echo "--- 6. STORES ---" + +check "GET /stores" 200 GET "/stores" +check "GET /stores/1" 200 GET "/stores/1" +check "GET /stores/999999" 404 GET "/stores/999999" + +check "Create store (admin)" 201 POST "/stores" "${A[@]}" "${J[@]}" -d '{"storeName":"QA_TEST_Store","address":"123 QA St","city":"Testville","state":"QA","zipCode":"00000","phone":"5559999999"}' +STORE_ID=$(id_from_body) +[ -n "$STORE_ID" ] && CLEANUP_STORE_IDS+=("$STORE_ID") + +if [ -n "$STORE_ID" ]; then + check "Update store" 200 PUT "/stores/$STORE_ID" "${A[@]}" "${J[@]}" -d "{\"storeName\":\"QA_TEST_StoreUpd\",\"address\":\"456 QA Ave\",\"city\":\"Testville\",\"state\":\"QA\",\"zipCode\":\"00001\",\"phone\":\"5559999998\"}" +fi + +check "Create store to delete" 201 POST "/stores" "${A[@]}" "${J[@]}" -d '{"storeName":"QA_TEST_StoreDel","address":"789 Del Rd","city":"Gone","state":"QA","zipCode":"00002","phone":"5559999997"}' +DEL_STORE_ID=$(id_from_body) +if [ -n "$DEL_STORE_ID" ]; then + check "Delete store" 200 DELETE "/stores/$DEL_STORE_ID" "${A[@]}" +fi + +check "Create store (staff)" 403 POST "/stores" "${S[@]}" "${J[@]}" -d '{"storeName":"QA_TEST_Nope","address":"x","city":"x","state":"x","zipCode":"x","phone":"x"}' + +echo "--- 7. USERS / EMPLOYEES / CUSTOMERS ---" + +check "GET /users (admin)" 200 GET "/users" "${A[@]}" +check "GET /users (customer)" 403 GET "/users" "${C[@]}" +check "GET /users/1" 200 GET "/users/1" "${A[@]}" +check "GET /users/999999" 404 GET "/users/999999" "${A[@]}" + +check "GET /employees (admin)" 200 GET "/employees" "${A[@]}" +check "GET /employees (staff)" 403 GET "/employees" "${S[@]}" +check "GET /customers (staff)" 200 GET "/customers" "${S[@]}" +check "GET /customers (customer)" 403 GET "/customers" "${C[@]}" + +check "Admin create user" 201 POST "/users" "${A[@]}" "${J[@]}" -d '{"username":"qa_test_user2","password":"Test1234!","email":"qa2@test.com","firstName":"QA2","lastName":"Test2","phone":"5552222222","role":"CUSTOMER"}' +NEW_USER_ID=$(id_from_body) +[ -n "$NEW_USER_ID" ] && CLEANUP_USER_IDS+=("$NEW_USER_ID") +check_field "Created username" ".username" "qa_test_user2" + +if [ -n "$NEW_USER_ID" ]; then + check "Update user" 200 PUT "/users/$NEW_USER_ID" "${A[@]}" "${J[@]}" -d "{\"username\":\"qa_test_user2\",\"email\":\"qa2upd@test.com\",\"firstName\":\"QA2U\",\"lastName\":\"Test2U\",\"phone\":\"5552222223\",\"role\":\"CUSTOMER\"}" +fi + +check "GET /users/1/avatar/file" 200 GET "/users/1/avatar/file" "${A[@]}" +check "GET /users/50/avatar/file (default)" 200 GET "/users/50/avatar/file" "${A[@]}" + +check "GET /users no auth" 401 GET "/users" +check "Create user (staff)" 403 POST "/users" "${S[@]}" "${J[@]}" -d '{"username":"qa_nope","password":"x","email":"n@n.com","firstName":"N","lastName":"N","phone":"0","role":"CUSTOMER"}' +check "Create user (customer)" 403 POST "/users" "${C[@]}" "${J[@]}" -d '{"username":"qa_nope2","password":"x","email":"n2@n.com","firstName":"N","lastName":"N","phone":"0","role":"CUSTOMER"}' + +echo "--- 8. APPOINTMENTS ---" + +check "GET /appointments (admin)" 200 GET "/appointments" "${A[@]}" +check "GET /appointments/1" 200 GET "/appointments/1" "${A[@]}" +check "GET /appointments?storeId=1" 200 GET "/appointments?storeId=1" "${A[@]}" +check "GET /appointments?status=Scheduled" 200 GET "/appointments?status=Scheduled" "${A[@]}" +check "GET /appointments/availability" 200 GET "/appointments/availability?storeId=1&serviceId=1&date=2027-06-01" "${A[@]}" +check "GET /appointments/999999" 404 GET "/appointments/999999" "${A[@]}" + +check "Create appointment" 201 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-06-15","appointmentTime":"10:00","notes":"QA_TEST"}' +APPT_ID=$(id_from_body) +[ -n "$APPT_ID" ] && CLEANUP_APPT_IDS+=("$APPT_ID") +check_field "Appointment status" ".status" "Scheduled" + +if [ -n "$APPT_ID" ]; then + check "Cancel appointment" 200 PUT "/appointments/$APPT_ID" "${A[@]}" "${J[@]}" -d "{\"petId\":53,\"customerId\":32,\"storeId\":2,\"employeeId\":7,\"serviceId\":1,\"appointmentDate\":\"2027-06-15\",\"appointmentTime\":\"10:00\",\"status\":\"Cancelled\",\"notes\":\"QA_TEST cancelled\"}" + check_field "Cancelled status" ".status" "Cancelled" +fi + +check "Create appointment to delete" 201 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-07-01","appointmentTime":"14:00","notes":"QA_TEST_DEL"}' +DEL_APPT_ID=$(id_from_body) +if [ -n "$DEL_APPT_ID" ]; then + check "Delete appointment" 200 DELETE "/appointments/$DEL_APPT_ID" "${A[@]}" +fi + +check "Appointment past date" 400 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2020-01-01","appointmentTime":"10:00","notes":"QA_TEST past"}' + +check "Pet-service mismatch" 400 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":38,"customerId":17,"storeId":1,"employeeId":4,"serviceId":1,"appointmentDate":"2027-08-01","appointmentTime":"10:00","notes":"QA_TEST mismatch"}' + +check "Create appt for duplicate test" 201 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-09-01","appointmentTime":"09:00","notes":"QA_TEST dup1"}' +DUP_APPT_ID=$(id_from_body) +[ -n "$DUP_APPT_ID" ] && CLEANUP_APPT_IDS+=("$DUP_APPT_ID") +check "Duplicate time/employee" 400 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-09-01","appointmentTime":"09:00","notes":"QA_TEST dup2"}' + +check "GET /appointments (customer)" 200 GET "/appointments" "${C[@]}" +check "GET /appointments (staff)" 200 GET "/appointments" "${S[@]}" +check "Create appointment (no auth)" 401 POST "/appointments" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":1,"employeeId":4,"serviceId":1,"appointmentDate":"2027-10-01","appointmentTime":"10:00"}' + +echo "--- 9. ADOPTIONS ---" + +check "GET /adoptions (admin)" 200 GET "/adoptions" "${A[@]}" +check "GET /adoptions/1" 200 GET "/adoptions/1" "${A[@]}" +check "GET /adoptions/999999" 404 GET "/adoptions/999999" "${A[@]}" + +AVAIL_PET_1="" +AVAIL_PET_2="" +AVAIL_PET_3="" +curl -s "$BASE/pets?status=Available&size=5" "${A[@]}" -o /tmp/qa_avail.json 2>/dev/null +AVAIL_PET_1=$(jq -r '.content[0].id // empty' /tmp/qa_avail.json 2>/dev/null) +AVAIL_PET_2=$(jq -r '.content[1].id // empty' /tmp/qa_avail.json 2>/dev/null) +AVAIL_PET_3=$(jq -r '.content[2].id // empty' /tmp/qa_avail.json 2>/dev/null) + +if [ -n "$AVAIL_PET_1" ]; then + check "Staff create adoption (Pending)" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_1,\"customerId\":32,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST adopt1\"}" + ADOPT_ID_1=$(id_from_body) + [ -n "$ADOPT_ID_1" ] && CLEANUP_ADOPTION_IDS+=("$ADOPT_ID_1") + + check "Pet now Pending" 200 GET "/pets/$AVAIL_PET_1" + check_field "Pet status Pending" ".status" "Pending" + + if [ -n "$ADOPT_ID_1" ]; then + check "Cancel adoption" 200 PUT "/adoptions/$ADOPT_ID_1" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_1,\"customerId\":32,\"storeId\":1,\"status\":\"Cancelled\",\"notes\":\"QA_TEST cancelled\"}" + check "Pet back to Available" 200 GET "/pets/$AVAIL_PET_1" + check_field "Pet status Available" ".status" "Available" + fi +fi + +if [ -n "$AVAIL_PET_2" ]; then + check "Staff create adoption 2" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_2,\"customerId\":33,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST adopt2\"}" + ADOPT_ID_2=$(id_from_body) + [ -n "$ADOPT_ID_2" ] && CLEANUP_ADOPTION_IDS+=("$ADOPT_ID_2") + + if [ -n "$ADOPT_ID_2" ]; then + check "Complete adoption" 200 PUT "/adoptions/$ADOPT_ID_2" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_2,\"customerId\":33,\"storeId\":1,\"status\":\"Completed\",\"notes\":\"QA_TEST completed\"}" + check "Pet now Adopted" 200 GET "/pets/$AVAIL_PET_2" + check_field "Pet status Adopted" ".status" "Adopted" + fi +fi + +if [ -n "$AVAIL_PET_3" ]; then + check "Customer request adoption" 201 POST "/adoptions" "${C[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"storeId\":1,\"notes\":\"QA_TEST customer adopt\"}" + ADOPT_ID_3=$(id_from_body) + [ -n "$ADOPT_ID_3" ] && CLEANUP_ADOPTION_IDS+=("$ADOPT_ID_3") + + if [ -n "$ADOPT_ID_3" ]; then + check "Cancel customer adoption" 200 PUT "/adoptions/$ADOPT_ID_3" "${C[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"storeId\":1,\"status\":\"Cancelled\",\"notes\":\"QA_TEST cust cancelled\"}" + check "Pet Available again" 200 GET "/pets/$AVAIL_PET_3" + check_field "Pet Available after cancel" ".status" "Available" + fi + + check "Adopt already pending" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"customerId\":34,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST pending block\"}" + ADOPT_BLOCK_ID=$(id_from_body) + [ -n "$ADOPT_BLOCK_ID" ] && CLEANUP_ADOPTION_IDS+=("$ADOPT_BLOCK_ID") + check "Duplicate adoption for pending pet" 400 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"customerId\":35,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST dup\"}" +fi + +check "GET /adoptions (staff)" 200 GET "/adoptions" "${S[@]}" +check "GET /adoptions (customer)" 200 GET "/adoptions" "${C[@]}" + +echo "--- 10. SALES ---" + +check "GET /sales (admin)" 200 GET "/sales" "${A[@]}" +check "GET /sales/1" 200 GET "/sales/1" "${A[@]}" +check "GET /sales/my (customer)" 200 GET "/sales/my" "${C[@]}" +check "GET /sales (customer)" 403 GET "/sales" "${C[@]}" +check "GET /sales/999999" 404 GET "/sales/999999" "${A[@]}" + +curl -s "$BASE/inventory?storeId=1" "${A[@]}" -o /tmp/qa_inv_before.json 2>/dev/null +INV_BEFORE=$(jq -r '.content[] | select(.productId == 1) | .quantity // 0' /tmp/qa_inv_before.json 2>/dev/null | head -1) + +check "Staff create sale" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":1,"quantity":1}],"notes":"QA_TEST sale"}' +SALE_ID=$(id_from_body) +[ -n "$SALE_ID" ] && CLEANUP_SALE_IDS+=("$SALE_ID") + +if [ -n "$INV_BEFORE" ] && [ "$INV_BEFORE" != "" ] && [ "$INV_BEFORE" != "null" ]; then + curl -s "$BASE/inventory?storeId=1" "${A[@]}" -o /tmp/qa_inv_after.json 2>/dev/null + INV_AFTER=$(jq -r '.content[] | select(.productId == 1) | .quantity // 0' /tmp/qa_inv_after.json 2>/dev/null | head -1) + if [ -n "$INV_AFTER" ] && [ "$INV_AFTER" != "null" ] && [ "$((INV_BEFORE - 1))" = "$INV_AFTER" ]; then + PASS=$((PASS+1)) + else + FAIL=$((FAIL+1)); echo "FAIL: Inventory decrease — before=$INV_BEFORE after=$INV_AFTER" + fi +fi + +check "Sale non-existent product" 404 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":999999,"quantity":1}]}' +check "Sale zero quantity" 400 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":1,"quantity":0}]}' +check "Sale (no auth)" 401 POST "/sales" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":1,"quantity":1}]}' +check "Sale (customer)" 403 POST "/sales" "${C[@]}" "${J[@]}" -d '{"storeId":1,"customerId":15,"items":[{"productId":1,"quantity":1}]}' + +check "Staff create sale 2" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":2,"quantity":1}],"notes":"QA_TEST sale2"}' +SALE_ID_2=$(id_from_body) +[ -n "$SALE_ID_2" ] && CLEANUP_SALE_IDS+=("$SALE_ID_2") + +check "GET /sales filtered by store" 200 GET "/sales?storeId=1" "${A[@]}" +check "GET /sales paginated" 200 GET "/sales?page=0&size=5" "${A[@]}" + +echo "--- 11. CART ---" + +check "GET /cart (customer)" 200 GET "/cart?storeId=1" "${C[@]}" + +check "Add cart item" 200 POST "/cart/items" "${C[@]}" "${J[@]}" -d '{"productId":1,"storeId":1,"quantity":1}' +if [ "$(jq -r '.status // empty' /tmp/qa_body.json 2>/dev/null)" = "" ]; then + check "Add cart item alt" 201 POST "/cart/items" "${C[@]}" "${J[@]}" -d '{"productId":2,"storeId":1,"quantity":1}' +fi + +check "GET cart with item" 200 GET "/cart?storeId=1" "${C[@]}" +CART_SUBTOTAL=$(jq -r '.subtotal // .totalBeforeDiscount // 0' /tmp/qa_body.json 2>/dev/null) +CART_ITEM_ID=$(jq -r '.items[0].id // .items[0].cartItemId // empty' /tmp/qa_body.json 2>/dev/null) + +if [ -n "$CART_ITEM_ID" ]; then + check "Update cart qty" 200 PUT "/cart/items/$CART_ITEM_ID" "${C[@]}" "${J[@]}" -d '{"quantity":2}' + check "GET cart after qty update" 200 GET "/cart?storeId=1" "${C[@]}" +fi + +check "Apply coupon WELCOME10" 200 POST "/cart/coupon" "${C[@]}" "${J[@]}" -d '{"couponCode":"WELCOME10","storeId":1}' +check "GET cart with coupon" 200 GET "/cart?storeId=1" "${C[@]}" +DISCOUNT=$(jq -r '.discount // .totalDiscount // 0' /tmp/qa_body.json 2>/dev/null) + +check "Remove coupon" 200 DELETE "/cart/coupon?storeId=1" "${C[@]}" +check "GET cart no coupon" 200 GET "/cart?storeId=1" "${C[@]}" + +check "Apply points" 200 POST "/cart/points" "${C[@]}" "${J[@]}" -d '{"points":10,"storeId":1}' +check "Remove points" 200 DELETE "/cart/points?storeId=1" "${C[@]}" + +if [ -n "$CART_ITEM_ID" ]; then + check "Remove cart item" 200 DELETE "/cart/items/$CART_ITEM_ID" "${C[@]}" +fi + +check "Clear cart" 200 DELETE "/cart?storeId=1" "${C[@]}" + +check "Add item qty 0" 400 POST "/cart/items" "${C[@]}" "${J[@]}" -d '{"productId":1,"storeId":1,"quantity":0}' +check "Apply invalid coupon" 400 POST "/cart/coupon" "${C[@]}" "${J[@]}" -d '{"couponCode":"FAKECOUPON999","storeId":1}' +check "Checkout empty cart" 400 POST "/cart/checkout" "${C[@]}" "${J[@]}" -d '{"storeId":1}' + +check "Add item for checkout test" 200 POST "/cart/items" "${C[@]}" "${J[@]}" -d '{"productId":1,"storeId":1,"quantity":1}' +check "Checkout" 200 POST "/cart/checkout" "${C[@]}" "${J[@]}" -d '{"storeId":1}' +CHECKOUT_SALE_ID=$(jq -r '.id // .saleId // empty' /tmp/qa_body.json 2>/dev/null) +[ -n "$CHECKOUT_SALE_ID" ] && CLEANUP_SALE_IDS+=("$CHECKOUT_SALE_ID") + +check "Clear cart final" 200 DELETE "/cart?storeId=1" "${C[@]}" + +echo "--- 12. REFUNDS ---" + +check "GET /refunds (admin)" 200 GET "/refunds" "${A[@]}" +check "GET /refunds (customer)" 200 GET "/refunds" "${C[@]}" + +CUST_SALE_ID="" +if [ -n "$CHECKOUT_SALE_ID" ]; then + CUST_SALE_ID="$CHECKOUT_SALE_ID" +else + curl -s "$BASE/sales/my" "${C[@]}" -o /tmp/qa_my_sales.json 2>/dev/null + CUST_SALE_ID=$(jq -r '.content[0].id // .[0].id // empty' /tmp/qa_my_sales.json 2>/dev/null) +fi + +if [ -n "$CUST_SALE_ID" ]; then + check "Customer create refund" 201 POST "/refunds" "${C[@]}" "${J[@]}" -d "{\"saleId\":$CUST_SALE_ID,\"reason\":\"QA_TEST refund reason\"}" + REFUND_ID=$(id_from_body) + + if [ -n "$REFUND_ID" ]; then + check "Staff approve refund" 200 PUT "/refunds/$REFUND_ID" "${S[@]}" "${J[@]}" -d "{\"status\":\"Approved\",\"saleId\":$CUST_SALE_ID,\"reason\":\"QA_TEST approved\"}" + check_field "Refund approved" ".status" "Approved" + fi +fi + +check "Refund non-existent sale" 404 POST "/refunds" "${C[@]}" "${J[@]}" -d '{"saleId":999999,"reason":"QA_TEST nope"}' +check "Refund missing reason" 400 POST "/refunds" "${C[@]}" "${J[@]}" -d '{"saleId":1}' +check "GET /refunds (staff)" 200 GET "/refunds" "${S[@]}" +check "GET /refunds paginated" 200 GET "/refunds?page=0&size=5" "${A[@]}" + +echo "--- 13. COUPONS ---" + +check "GET /coupons (admin)" 200 GET "/coupons" "${A[@]}" +check "GET /coupons/1" 200 GET "/coupons/1" "${A[@]}" +check "GET /coupons/code/WELCOME10" 200 GET "/coupons/code/WELCOME10" "${A[@]}" +check "GET /coupons (customer)" 403 GET "/coupons" "${C[@]}" + +check "Create coupon" 201 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"QA_TEST_CPN","discountPercent":10,"description":"QA test coupon","expiryDate":"2028-12-31","maxUses":100}' +CPN_ID=$(id_from_body) +[ -n "$CPN_ID" ] && CLEANUP_COUPON_IDS+=("$CPN_ID") +check_field "Coupon code" ".code" "QA_TEST_CPN" + +if [ -n "$CPN_ID" ]; then + check "Update coupon" 200 PUT "/coupons/$CPN_ID" "${A[@]}" "${J[@]}" -d "{\"code\":\"QA_TEST_CPN\",\"discountPercent\":15,\"description\":\"updated\",\"expiryDate\":\"2028-12-31\",\"maxUses\":50}" +fi + +check "Create coupon to delete" 201 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"QA_TEST_DEL","discountPercent":5,"description":"del","expiryDate":"2028-12-31","maxUses":10}' +DEL_CPN_ID=$(id_from_body) +if [ -n "$DEL_CPN_ID" ]; then + check "Delete coupon" 200 DELETE "/coupons/$DEL_CPN_ID" "${A[@]}" +fi + +check "Coupon 100% discount" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"QA_TEST_100","discountPercent":100,"description":"bad","expiryDate":"2028-12-31","maxUses":1}' +check "Coupon 150% discount" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"QA_TEST_150","discountPercent":150,"description":"bad","expiryDate":"2028-12-31","maxUses":1}' + +check "Coupon 99.99%" 201 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"QA_TEST_99","discountPercent":99.99,"description":"boundary","expiryDate":"2028-12-31","maxUses":1}' +BOUNDARY_CPN_ID=$(id_from_body) +[ -n "$BOUNDARY_CPN_ID" ] && CLEANUP_COUPON_IDS+=("$BOUNDARY_CPN_ID") + +check "Duplicate coupon code" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"WELCOME10","discountPercent":5,"description":"dup","expiryDate":"2028-12-31","maxUses":1}' +check "Create coupon (customer)" 403 POST "/coupons" "${C[@]}" "${J[@]}" -d '{"code":"QA_TEST_NOPE","discountPercent":5,"description":"nope","expiryDate":"2028-12-31","maxUses":1}' + +echo "--- 14. SUPPLIERS / PRODUCT-SUPPLIERS / INVENTORY / PURCHASE-ORDERS ---" + +check "GET /suppliers (admin)" 200 GET "/suppliers" "${A[@]}" +check "GET /suppliers/1" 200 GET "/suppliers/1" "${A[@]}" +check "GET /suppliers (customer)" 403 GET "/suppliers" "${C[@]}" + +check "Create supplier" 201 POST "/suppliers" "${A[@]}" "${J[@]}" -d '{"supplierName":"QA_TEST_Supplier","contactName":"QA Contact","email":"qa_supplier@test.com","phone":"5553333333","address":"100 Supply Rd"}' +SUPP_ID=$(id_from_body) +[ -n "$SUPP_ID" ] && CLEANUP_SUPPLIER_IDS+=("$SUPP_ID") + +if [ -n "$SUPP_ID" ]; then + check "Update supplier" 200 PUT "/suppliers/$SUPP_ID" "${A[@]}" "${J[@]}" -d "{\"supplierName\":\"QA_TEST_SuppUpd\",\"contactName\":\"QA Updated\",\"email\":\"qa_supp_upd@test.com\",\"phone\":\"5553333334\",\"address\":\"101 Supply Rd\"}" +fi + +check "Create supplier to delete" 201 POST "/suppliers" "${A[@]}" "${J[@]}" -d '{"supplierName":"QA_TEST_SuppDel","contactName":"Del","email":"del@test.com","phone":"5550000000","address":"x"}' +DEL_SUPP_ID=$(id_from_body) +if [ -n "$DEL_SUPP_ID" ]; then + check "Delete supplier" 200 DELETE "/suppliers/$DEL_SUPP_ID" "${A[@]}" +fi + +check "GET /product-suppliers (admin)" 200 GET "/product-suppliers" "${A[@]}" +check "GET /inventory (admin)" 200 GET "/inventory" "${A[@]}" +check "GET /inventory/1" 200 GET "/inventory/1" "${A[@]}" +check "GET /purchase-orders (admin)" 200 GET "/purchase-orders" "${A[@]}" +check "GET /purchase-orders (customer)" 403 GET "/purchase-orders" "${C[@]}" +check "GET /inventory (customer)" 403 GET "/inventory" "${C[@]}" + +echo "--- 15. CHAT ---" + +check "GET /chat/conversations (customer)" 200 GET "/chat/conversations" "${C[@]}" + +check "Create conversation" 201 POST "/chat/conversations" "${C[@]}" "${J[@]}" -d '{}' +CONV_ID=$(id_from_body) +[ -n "$CONV_ID" ] && CLEANUP_CONV_IDS+=("$CONV_ID") + +if [ -n "$CONV_ID" ]; then + check "Send message" 201 POST "/chat/conversations/$CONV_ID/messages" "${C[@]}" "${J[@]}" -d '{"content":"Hello from QA_TEST"}' + + check "Send empty message" 400 POST "/chat/conversations/$CONV_ID/messages" "${C[@]}" "${J[@]}" -d '{"content":""}' + check "Send null content" 400 POST "/chat/conversations/$CONV_ID/messages" "${C[@]}" "${J[@]}" -d '{}' + + check "GET messages" 200 GET "/chat/conversations/$CONV_ID/messages" "${C[@]}" + + check "Request human takeover" 200 POST "/chat/conversations/$CONV_ID/takeover" "${C[@]}" "${J[@]}" -d '{}' + + check "Staff view conversations" 200 GET "/chat/conversations" "${S[@]}" + + check "Staff send reply" 201 POST "/chat/conversations/$CONV_ID/messages" "${S[@]}" "${J[@]}" -d '{"content":"Staff reply QA_TEST"}' + + check "GET conversation detail" 200 GET "/chat/conversations/$CONV_ID" "${S[@]}" + + check "Close conversation" 200 PUT "/chat/conversations/$CONV_ID/close" "${S[@]}" "${J[@]}" -d '{}' + check "GET closed conversation" 200 GET "/chat/conversations/$CONV_ID" "${C[@]}" + check_field "Conversation closed" ".status" "CLOSED" + + check "Send to closed" 400 POST "/chat/conversations/$CONV_ID/messages" "${C[@]}" "${J[@]}" -d '{"content":"should fail"}' +fi + +check "GET /chat/conversations (admin)" 200 GET "/chat/conversations" "${A[@]}" +check "Create conv 2" 201 POST "/chat/conversations" "${C[@]}" "${J[@]}" -d '{}' +CONV_ID_2=$(id_from_body) +[ -n "$CONV_ID_2" ] && CLEANUP_CONV_IDS+=("$CONV_ID_2") +check "Send message conv 2" 201 POST "/chat/conversations/$CONV_ID_2/messages" "${C[@]}" "${J[@]}" -d '{"content":"QA_TEST second conv"}' + +echo "--- 16. ACTIVITY LOGS / ANALYTICS / DROPDOWNS / HEALTH ---" + +check "GET /health" 200 GET "/health" +check "GET /activity-logs (admin)" 200 GET "/activity-logs" "${A[@]}" +check "GET /activity-logs (customer)" 403 GET "/activity-logs" "${C[@]}" +check "GET /analytics/dashboard (admin)" 200 GET "/analytics/dashboard" "${A[@]}" +check "GET /analytics/dashboard (customer)" 403 GET "/analytics/dashboard" "${C[@]}" +check "GET /analytics/dashboard?days=1" 200 GET "/analytics/dashboard?days=1" "${A[@]}" + +for ep in "dropdowns/pets" "dropdowns/services" "dropdowns/products" "dropdowns/categories" \ + "dropdowns/product-categories" "dropdowns/pet-species" "dropdowns/pet-breeds" \ + "dropdowns/stores" "dropdowns/customers" "dropdowns/adoption-pets" \ + "dropdowns/employees" "dropdowns/suppliers"; do + check "GET /$ep" 200 GET "/$ep" "${A[@]}" +done + +check "GET /dropdowns/stores/1/employees" 200 GET "/dropdowns/stores/1/employees" "${A[@]}" +check "GET /dropdowns/customers/32/pets" 200 GET "/dropdowns/customers/32/pets" "${A[@]}" + +echo "--- 17. CROSS-ENTITY ---" + +curl -s "$BASE/inventory?storeId=1" "${A[@]}" -o /tmp/qa_cross_inv1.json 2>/dev/null +CROSS_INV_BEFORE=$(jq -r '.content[] | select(.productId == 2) | .quantity // 0' /tmp/qa_cross_inv1.json 2>/dev/null | head -1) + +check "Cross: create sale" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":2,"quantity":1}],"notes":"QA_TEST_CROSS"}' +CROSS_SALE_ID=$(id_from_body) +[ -n "$CROSS_SALE_ID" ] && CLEANUP_SALE_IDS+=("$CROSS_SALE_ID") + +if [ -n "$CROSS_INV_BEFORE" ] && [ "$CROSS_INV_BEFORE" != "null" ] && [ "$CROSS_INV_BEFORE" != "" ]; then + curl -s "$BASE/inventory?storeId=1" "${A[@]}" -o /tmp/qa_cross_inv2.json 2>/dev/null + CROSS_INV_AFTER=$(jq -r '.content[] | select(.productId == 2) | .quantity // 0' /tmp/qa_cross_inv2.json 2>/dev/null | head -1) + if [ "$((CROSS_INV_BEFORE - 1))" = "$CROSS_INV_AFTER" ]; then + PASS=$((PASS+1)) + else + FAIL=$((FAIL+1)); echo "FAIL: Cross inventory decrease — before=$CROSS_INV_BEFORE after=$CROSS_INV_AFTER" + fi +fi + +if [ -n "$CROSS_SALE_ID" ]; then + check "Cross: create refund" 201 POST "/refunds" "${C[@]}" "${J[@]}" -d "{\"saleId\":$CROSS_SALE_ID,\"reason\":\"QA_TEST cross refund\"}" + CROSS_REFUND_ID=$(id_from_body) + if [ -n "$CROSS_REFUND_ID" ]; then + check "Cross: approve refund" 200 PUT "/refunds/$CROSS_REFUND_ID" "${S[@]}" "${J[@]}" -d "{\"status\":\"Approved\",\"saleId\":$CROSS_SALE_ID,\"reason\":\"QA_TEST cross approved\"}" + curl -s "$BASE/inventory?storeId=1" "${A[@]}" -o /tmp/qa_cross_inv3.json 2>/dev/null + CROSS_INV_RESTORED=$(jq -r '.content[] | select(.productId == 2) | .quantity // 0' /tmp/qa_cross_inv3.json 2>/dev/null | head -1) + if [ -n "$CROSS_INV_RESTORED" ] && [ "$CROSS_INV_RESTORED" = "$CROSS_INV_BEFORE" ]; then + PASS=$((PASS+1)) + else + FAIL=$((FAIL+1)); echo "FAIL: Cross inventory restore — expected=$CROSS_INV_BEFORE got=$CROSS_INV_RESTORED" + fi + fi +fi + +curl -s "$BASE/pets?status=Available&size=5&page=1" "${A[@]}" -o /tmp/qa_cross_avail.json 2>/dev/null +CROSS_PET=$(jq -r '.content[0].id // empty' /tmp/qa_cross_avail.json 2>/dev/null) + +if [ -n "$CROSS_PET" ]; then + check "Cross: create adoption Pending" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET,\"customerId\":35,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST cross adopt\"}" + CROSS_ADOPT_ID=$(id_from_body) + [ -n "$CROSS_ADOPT_ID" ] && CLEANUP_ADOPTION_IDS+=("$CROSS_ADOPT_ID") + + check "Cross: pet is Pending" 200 GET "/pets/$CROSS_PET" + check_field "Cross pet Pending" ".status" "Pending" + + if [ -n "$CROSS_ADOPT_ID" ]; then + check "Cross: complete adoption" 200 PUT "/adoptions/$CROSS_ADOPT_ID" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET,\"customerId\":35,\"storeId\":1,\"status\":\"Completed\",\"notes\":\"QA_TEST cross completed\"}" + check "Cross: pet is Adopted" 200 GET "/pets/$CROSS_PET" + check_field "Cross pet Adopted" ".status" "Adopted" + fi +fi + +curl -s "$BASE/pets?status=Available&size=5&page=2" "${A[@]}" -o /tmp/qa_cross_avail2.json 2>/dev/null +CROSS_PET_2=$(jq -r '.content[0].id // empty' /tmp/qa_cross_avail2.json 2>/dev/null) + +if [ -n "$CROSS_PET_2" ]; then + check "Cross: adopt then cancel" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET_2,\"customerId\":36,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST cross cancel\"}" + CROSS_ADOPT_ID_2=$(id_from_body) + [ -n "$CROSS_ADOPT_ID_2" ] && CLEANUP_ADOPTION_IDS+=("$CROSS_ADOPT_ID_2") + + if [ -n "$CROSS_ADOPT_ID_2" ]; then + check "Cross: cancel adoption" 200 PUT "/adoptions/$CROSS_ADOPT_ID_2" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET_2,\"customerId\":36,\"storeId\":1,\"status\":\"Cancelled\",\"notes\":\"QA_TEST cross cancelled\"}" + check "Cross: pet Available" 200 GET "/pets/$CROSS_PET_2" + check_field "Cross pet Available" ".status" "Available" + fi +fi + +echo "" +echo "--- CLEANUP ---" + +for id in "${CLEANUP_CONV_IDS[@]}"; do + curl -s -o /dev/null -X PUT "$BASE/chat/conversations/$id/close" "${S[@]}" "${J[@]}" -d '{}' 2>/dev/null +done + +for id in "${CLEANUP_ADOPTION_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/adoptions/$id" "${A[@]}" 2>/dev/null +done + +for id in "${CLEANUP_APPT_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/appointments/$id" "${A[@]}" 2>/dev/null +done + +for id in "${CLEANUP_SALE_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/sales/$id" "${A[@]}" 2>/dev/null +done + +for id in "${CLEANUP_MYPET_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/my-pets/$id" "${C[@]}" 2>/dev/null +done + +for id in "${CLEANUP_PET_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/pets/$id" "${A[@]}" 2>/dev/null +done + +for id in "${CLEANUP_SERVICE_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/services/$id" "${A[@]}" 2>/dev/null +done + +for id in "${CLEANUP_PRODUCT_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/products/$id" "${A[@]}" 2>/dev/null +done + +for id in "${CLEANUP_CATEGORY_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/categories/$id" "${A[@]}" 2>/dev/null +done + +for id in "${CLEANUP_STORE_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/stores/$id" "${A[@]}" 2>/dev/null +done + +for id in "${CLEANUP_COUPON_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/coupons/$id" "${A[@]}" 2>/dev/null +done + +for id in "${CLEANUP_SUPPLIER_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/suppliers/$id" "${A[@]}" 2>/dev/null +done + +for id in "${CLEANUP_USER_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/users/$id" "${A[@]}" 2>/dev/null +done + +curl -s -o /dev/null -X DELETE "$BASE/users" "${A[@]}" -G -d "username=testuser_qa" 2>/dev/null + +rm -f /tmp/qa_body.json /tmp/qa_avail.json /tmp/qa_inv_before.json /tmp/qa_inv_after.json +rm -f /tmp/qa_my_sales.json /tmp/qa_cross_inv1.json /tmp/qa_cross_inv2.json /tmp/qa_cross_inv3.json +rm -f /tmp/qa_cross_avail.json /tmp/qa_cross_avail2.json + +echo "" +echo "=========================================" +echo "RESULTS: $PASS passed, $FAIL failed" +echo "=========================================" -- 2.49.1 From 0b8bf02bd4a9c7aa830a68f73d8dc6ba8a45982a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 07:33:05 -0600 Subject: [PATCH 24/34] Fix test DTO fields --- test-backend-full.sh | 234 +++++++++++++++++++++---------------------- 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/test-backend-full.sh b/test-backend-full.sh index ad8cbf75..21535d77 100755 --- a/test-backend-full.sh +++ b/test-backend-full.sh @@ -50,7 +50,7 @@ check_field_gt() { else FAIL=$((FAIL+1)); echo "FAIL: $label — expected > $threshold got '$actual'"; fi } -id_from_body() { jq -r '.id // .userId // empty' /tmp/qa_body.json 2>/dev/null; } +id_from_body() { jq -r '.id // .userId // .prodId // .categoryId // .petId // .serviceId // .storeId // .appointmentId // .adoptionId // .saleId // .couponId // .supId // empty' /tmp/qa_body.json 2>/dev/null; } echo "=========================================" echo " PET SHOP QA — Full Backend Test Suite" @@ -109,21 +109,21 @@ check "GET /products?q=dog" 200 GET "/products?q=dog" check "GET /products?categoryId=1" 200 GET "/products?categoryId=1" check "GET /products/999999" 404 GET "/products/999999" -check "Create product (admin)" 201 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_TEST_Product","description":"QA test","price":9.99,"categoryId":1}' +check "Create product (admin)" 201 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_TEST_Product","prodDesc":"QA test","prodPrice":9.99,"categoryId":1}' PROD_ID=$(id_from_body) [ -n "$PROD_ID" ] && CLEANUP_PRODUCT_IDS+=("$PROD_ID") check_field "Product name" ".prodName" "QA_TEST_Product" -check "Create product (customer)" 403 POST "/products" "${C[@]}" "${J[@]}" -d '{"prodName":"QA_TEST_Nope","price":1,"categoryId":1}' -check "Create product (no auth)" 401 POST "/products" "${J[@]}" -d '{"prodName":"QA_TEST_Nope2","price":1,"categoryId":1}' -check "Create product missing name" 400 POST "/products" "${A[@]}" "${J[@]}" -d '{"price":1,"categoryId":1}' +check "Create product (customer)" 403 POST "/products" "${C[@]}" "${J[@]}" -d '{"prodName":"QA_TEST_Nope","prodPrice":1,"categoryId":1}' +check "Create product (no auth)" 401 POST "/products" "${J[@]}" -d '{"prodName":"QA_TEST_Nope2","prodPrice":1,"categoryId":1}' +check "Create product missing name" 400 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodPrice":1,"categoryId":1}' if [ -n "$PROD_ID" ]; then - check "Update product" 200 PUT "/products/$PROD_ID" "${A[@]}" "${J[@]}" -d "{\"prodName\":\"QA_TEST_Updated\",\"description\":\"updated\",\"price\":19.99,\"categoryId\":1}" + check "Update product" 200 PUT "/products/$PROD_ID" "${A[@]}" "${J[@]}" -d "{\"prodName\":\"QA_TEST_Updated\",\"prodDesc\":\"updated\",\"prodPrice\":19.99,\"categoryId\":1}" check_field "Updated name" ".prodName" "QA_TEST_Updated" fi -check "Create product to delete" 201 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_TEST_DeleteMe","description":"del","price":1,"categoryId":1}' +check "Create product to delete" 201 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_TEST_DeleteMe","prodDesc":"del","prodPrice":1,"categoryId":1}' DEL_PROD_ID=$(id_from_body) if [ -n "$DEL_PROD_ID" ]; then check "Delete product" 200 DELETE "/products/$DEL_PROD_ID" "${A[@]}" @@ -131,7 +131,7 @@ if [ -n "$DEL_PROD_ID" ]; then fi check "GET /products paginated" 200 GET "/products?page=0&size=5" -check "GET /products sorted" 200 GET "/products?sort=price,desc" +check "GET /products sorted" 200 GET "/products?sort=prodName,asc" echo "--- 3. CATEGORIES ---" @@ -139,19 +139,19 @@ check "GET /categories" 200 GET "/categories" check "GET /categories/1" 200 GET "/categories/1" check "GET /categories/999999" 404 GET "/categories/999999" -check "Create category (admin)" 201 POST "/categories" "${A[@]}" "${J[@]}" -d '{"catName":"QA_TEST_Category","description":"qa"}' +check "Create category (admin)" 201 POST "/categories" "${A[@]}" "${J[@]}" -d '{"categoryName":"QA_TEST_Category","categoryType":"qa"}' CAT_ID=$(id_from_body) [ -n "$CAT_ID" ] && CLEANUP_CATEGORY_IDS+=("$CAT_ID") -check "Create category (customer)" 403 POST "/categories" "${C[@]}" "${J[@]}" -d '{"catName":"QA_TEST_Nope"}' -check "Create category missing name" 400 POST "/categories" "${A[@]}" "${J[@]}" -d '{"description":"no name"}' +check "Create category (customer)" 403 POST "/categories" "${C[@]}" "${J[@]}" -d '{"categoryName":"QA_TEST_Nope"}' +check "Create category missing name" 400 POST "/categories" "${A[@]}" "${J[@]}" -d '{"categoryType":"no name"}' if [ -n "$CAT_ID" ]; then - check "Update category" 200 PUT "/categories/$CAT_ID" "${A[@]}" "${J[@]}" -d "{\"catName\":\"QA_TEST_CatUpdated\",\"description\":\"updated\"}" - check_field "Updated cat name" ".catName" "QA_TEST_CatUpdated" + check "Update category" 200 PUT "/categories/$CAT_ID" "${A[@]}" "${J[@]}" -d "{\"categoryName\":\"QA_TEST_CatUpdated\",\"categoryType\":\"updated\"}" + check_field "Updated cat name" ".categoryName" "QA_TEST_CatUpdated" fi -check "Create category to delete" 201 POST "/categories" "${A[@]}" "${J[@]}" -d '{"catName":"QA_TEST_CatDel","description":"del"}' +check "Create category to delete" 201 POST "/categories" "${A[@]}" "${J[@]}" -d '{"categoryName":"QA_TEST_CatDel","categoryType":"del"}' DEL_CAT_ID=$(id_from_body) if [ -n "$DEL_CAT_ID" ]; then check "Delete category" 200 DELETE "/categories/$DEL_CAT_ID" "${A[@]}" @@ -167,31 +167,31 @@ check "GET /pets?status=Available" 200 GET "/pets?status=Available" check "GET /pets/999999" 404 GET "/pets/999999" check "GET /my-pets (customer)" 200 GET "/my-pets" "${C[@]}" -check "Admin create pet" 201 POST "/pets" "${A[@]}" "${J[@]}" -d '{"name":"QA_TEST_Pet","species":"Dog","breed":"Labrador","age":2,"gender":"Male","status":"Available","storeId":1,"price":100,"description":"QA test pet"}' +check "Admin create pet" 201 POST "/pets" "${A[@]}" "${J[@]}" -d '{"petName":"QA_TEST_Pet","petSpecies":"Dog","petBreed":"Labrador","petAge":2,"petStatus":"Available","storeId":1,"petPrice":100}' PET_ID=$(id_from_body) [ -n "$PET_ID" ] && CLEANUP_PET_IDS+=("$PET_ID") -check_field "Pet name" ".name" "QA_TEST_Pet" -check_field "Pet species" ".species" "Dog" +check_field "Pet name" ".petName" "QA_TEST_Pet" +check_field "Pet species" ".petSpecies" "Dog" if [ -n "$PET_ID" ]; then - check "Update pet" 200 PUT "/pets/$PET_ID" "${A[@]}" "${J[@]}" -d "{\"name\":\"QA_TEST_PetUpd\",\"species\":\"Dog\",\"breed\":\"Labrador\",\"age\":3,\"gender\":\"Male\",\"status\":\"Available\",\"storeId\":1,\"price\":150,\"description\":\"updated\"}" - check_field "Updated pet name" ".name" "QA_TEST_PetUpd" + check "Update pet" 200 PUT "/pets/$PET_ID" "${A[@]}" "${J[@]}" -d "{\"petName\":\"QA_TEST_PetUpd\",\"petSpecies\":\"Dog\",\"petBreed\":\"Labrador\",\"petAge\":3,\"petStatus\":\"Available\",\"storeId\":1,\"petPrice\":150}" + check_field "Updated pet name" ".petName" "QA_TEST_PetUpd" fi -check "Admin create pet to delete" 201 POST "/pets" "${A[@]}" "${J[@]}" -d '{"name":"QA_TEST_PetDel","species":"Cat","breed":"Siamese","age":1,"gender":"Female","status":"Available","storeId":1,"price":50,"description":"del"}' +check "Admin create pet to delete" 201 POST "/pets" "${A[@]}" "${J[@]}" -d '{"petName":"QA_TEST_PetDel","petSpecies":"Cat","petBreed":"Siamese","petAge":1,"petStatus":"Available","storeId":1,"petPrice":50}' DEL_PET_ID=$(id_from_body) if [ -n "$DEL_PET_ID" ]; then check "Delete pet" 200 DELETE "/pets/$DEL_PET_ID" "${A[@]}" check "GET deleted pet" 404 GET "/pets/$DEL_PET_ID" fi -check "Customer create my-pet" 201 POST "/my-pets" "${C[@]}" "${J[@]}" -d '{"name":"QA_TEST_MyPet","species":"Rabbit","breed":"Holland Lop","age":1,"gender":"Female","description":"my pet"}' +check "Customer create my-pet" 201 POST "/my-pets" "${C[@]}" "${J[@]}" -d '{"petName":"QA_TEST_MyPet","species":"Rabbit","breed":"Holland Lop","petAge":1}' MY_PET_ID=$(id_from_body) [ -n "$MY_PET_ID" ] && CLEANUP_MYPET_IDS+=("$MY_PET_ID") check "GET /my-pets has new pet" 200 GET "/my-pets" "${C[@]}" -check "Create pet (customer direct)" 403 POST "/pets" "${C[@]}" "${J[@]}" -d '{"name":"QA_TEST_Nope","species":"Dog","breed":"Lab","age":1,"gender":"Male","status":"Available","storeId":1,"price":10}' -check "Create pet (no auth)" 401 POST "/pets" "${J[@]}" -d '{"name":"QA_TEST_Nope2","species":"Dog","breed":"Lab","age":1,"gender":"Male","status":"Available","storeId":1,"price":10}' +check "Create pet (customer direct)" 403 POST "/pets" "${C[@]}" "${J[@]}" -d '{"petName":"QA_TEST_Nope","petSpecies":"Dog","petBreed":"Lab","petAge":1,"petStatus":"Available","storeId":1,"petPrice":10}' +check "Create pet (no auth)" 401 POST "/pets" "${J[@]}" -d '{"petName":"QA_TEST_Nope2","petSpecies":"Dog","petBreed":"Lab","petAge":1,"petStatus":"Available","storeId":1,"petPrice":10}' check "GET /pets paginated" 200 GET "/pets?page=0&size=5" echo "--- 5. SERVICES ---" @@ -201,21 +201,21 @@ check "GET /services/1" 200 GET "/services/1" check "GET /services?species=Dog" 200 GET "/services?species=Dog" check "GET /services/999999" 404 GET "/services/999999" -check "Create service (admin)" 201 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_TEST_Service","description":"qa svc","basePrice":25.00,"duration":30,"species":["Dog","Cat"]}' +check "Create service (admin)" 201 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_TEST_Service","serviceDesc":"qa svc","servicePrice":25.00,"serviceDuration":30,"species":["Dog","Cat"]}' SVC_ID=$(id_from_body) [ -n "$SVC_ID" ] && CLEANUP_SERVICE_IDS+=("$SVC_ID") check_field "Service name" ".serviceName" "QA_TEST_Service" if [ -n "$SVC_ID" ]; then - check "Update service" 200 PUT "/services/$SVC_ID" "${A[@]}" "${J[@]}" -d "{\"serviceName\":\"QA_TEST_SvcUpd\",\"description\":\"upd\",\"basePrice\":30,\"duration\":45,\"species\":[\"Dog\"]}" + check "Update service" 200 PUT "/services/$SVC_ID" "${A[@]}" "${J[@]}" -d "{\"serviceName\":\"QA_TEST_SvcUpd\",\"serviceDesc\":\"upd\",\"servicePrice\":30,\"serviceDuration\":45,\"species\":[\"Dog\"]}" fi -check "Create service to delete" 201 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_TEST_SvcDel","description":"del","basePrice":10,"duration":15,"species":["Cat"]}' +check "Create service to delete" 201 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_TEST_SvcDel","serviceDesc":"del","servicePrice":10,"serviceDuration":15,"species":["Cat"]}' DEL_SVC_ID=$(id_from_body) if [ -n "$DEL_SVC_ID" ]; then check "Delete service" 200 DELETE "/services/$DEL_SVC_ID" "${A[@]}" fi -check "Create service (customer)" 403 POST "/services" "${C[@]}" "${J[@]}" -d '{"serviceName":"QA_TEST_Nope","basePrice":10,"duration":10,"species":["Dog"]}' +check "Create service (customer)" 403 POST "/services" "${C[@]}" "${J[@]}" -d '{"serviceName":"QA_TEST_Nope","servicePrice":10,"serviceDuration":10,"species":["Dog"]}' echo "--- 6. STORES ---" @@ -223,21 +223,21 @@ check "GET /stores" 200 GET "/stores" check "GET /stores/1" 200 GET "/stores/1" check "GET /stores/999999" 404 GET "/stores/999999" -check "Create store (admin)" 201 POST "/stores" "${A[@]}" "${J[@]}" -d '{"storeName":"QA_TEST_Store","address":"123 QA St","city":"Testville","state":"QA","zipCode":"00000","phone":"5559999999"}' +check "Create store (admin)" 201 POST "/stores" "${A[@]}" "${J[@]}" -d '{"storeName":"QA_TEST_Store","address":"123 QA St","phone":"5559999999","email":"qastore@test.com"}' STORE_ID=$(id_from_body) [ -n "$STORE_ID" ] && CLEANUP_STORE_IDS+=("$STORE_ID") if [ -n "$STORE_ID" ]; then - check "Update store" 200 PUT "/stores/$STORE_ID" "${A[@]}" "${J[@]}" -d "{\"storeName\":\"QA_TEST_StoreUpd\",\"address\":\"456 QA Ave\",\"city\":\"Testville\",\"state\":\"QA\",\"zipCode\":\"00001\",\"phone\":\"5559999998\"}" + check "Update store" 200 PUT "/stores/$STORE_ID" "${A[@]}" "${J[@]}" -d "{\"storeName\":\"QA_TEST_StoreUpd\",\"address\":\"456 QA Ave\",\"phone\":\"5559999998\",\"email\":\"qastoreupd@test.com\"}" fi -check "Create store to delete" 201 POST "/stores" "${A[@]}" "${J[@]}" -d '{"storeName":"QA_TEST_StoreDel","address":"789 Del Rd","city":"Gone","state":"QA","zipCode":"00002","phone":"5559999997"}' +check "Create store to delete" 201 POST "/stores" "${A[@]}" "${J[@]}" -d '{"storeName":"QA_TEST_StoreDel","address":"789 Del Rd","phone":"5559999997","email":"qastoredel@test.com"}' DEL_STORE_ID=$(id_from_body) if [ -n "$DEL_STORE_ID" ]; then check "Delete store" 200 DELETE "/stores/$DEL_STORE_ID" "${A[@]}" fi -check "Create store (staff)" 403 POST "/stores" "${S[@]}" "${J[@]}" -d '{"storeName":"QA_TEST_Nope","address":"x","city":"x","state":"x","zipCode":"x","phone":"x"}' +check "Create store (staff)" 403 POST "/stores" "${S[@]}" "${J[@]}" -d '{"storeName":"QA_TEST_Nope","address":"x","phone":"x","email":"x@x.com"}' echo "--- 7. USERS / EMPLOYEES / CUSTOMERS ---" @@ -276,34 +276,34 @@ check "GET /appointments?status=Scheduled" 200 GET "/appointments?status=Schedul check "GET /appointments/availability" 200 GET "/appointments/availability?storeId=1&serviceId=1&date=2027-06-01" "${A[@]}" check "GET /appointments/999999" 404 GET "/appointments/999999" "${A[@]}" -check "Create appointment" 201 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-06-15","appointmentTime":"10:00","notes":"QA_TEST"}' +check "Create appointment" 201 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-06-15","appointmentTime":"10:00","appointmentStatus":"Scheduled"}' APPT_ID=$(id_from_body) [ -n "$APPT_ID" ] && CLEANUP_APPT_IDS+=("$APPT_ID") -check_field "Appointment status" ".status" "Scheduled" +check_field "Appointment status" ".appointmentStatus" "Scheduled" if [ -n "$APPT_ID" ]; then - check "Cancel appointment" 200 PUT "/appointments/$APPT_ID" "${A[@]}" "${J[@]}" -d "{\"petId\":53,\"customerId\":32,\"storeId\":2,\"employeeId\":7,\"serviceId\":1,\"appointmentDate\":\"2027-06-15\",\"appointmentTime\":\"10:00\",\"status\":\"Cancelled\",\"notes\":\"QA_TEST cancelled\"}" - check_field "Cancelled status" ".status" "Cancelled" + check "Cancel appointment" 200 PUT "/appointments/$APPT_ID" "${A[@]}" "${J[@]}" -d "{\"petId\":53,\"customerId\":32,\"storeId\":2,\"employeeId\":7,\"serviceId\":1,\"appointmentDate\":\"2027-06-15\",\"appointmentTime\":\"10:00\",\"appointmentStatus\":\"Cancelled\"}" + check_field "Cancelled status" ".appointmentStatus" "Cancelled" fi -check "Create appointment to delete" 201 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-07-01","appointmentTime":"14:00","notes":"QA_TEST_DEL"}' +check "Create appointment to delete" 201 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-07-01","appointmentTime":"14:00","appointmentStatus":"Scheduled"}' DEL_APPT_ID=$(id_from_body) if [ -n "$DEL_APPT_ID" ]; then check "Delete appointment" 200 DELETE "/appointments/$DEL_APPT_ID" "${A[@]}" fi -check "Appointment past date" 400 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2020-01-01","appointmentTime":"10:00","notes":"QA_TEST past"}' +check "Appointment past date" 400 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2020-01-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}' -check "Pet-service mismatch" 400 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":38,"customerId":17,"storeId":1,"employeeId":4,"serviceId":1,"appointmentDate":"2027-08-01","appointmentTime":"10:00","notes":"QA_TEST mismatch"}' +check "Pet-service mismatch" 400 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":38,"customerId":17,"storeId":1,"employeeId":4,"serviceId":1,"appointmentDate":"2027-08-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}' -check "Create appt for duplicate test" 201 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-09-01","appointmentTime":"09:00","notes":"QA_TEST dup1"}' +check "Create appt for duplicate test" 201 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-09-01","appointmentTime":"09:00","appointmentStatus":"Scheduled"}' DUP_APPT_ID=$(id_from_body) [ -n "$DUP_APPT_ID" ] && CLEANUP_APPT_IDS+=("$DUP_APPT_ID") -check "Duplicate time/employee" 400 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-09-01","appointmentTime":"09:00","notes":"QA_TEST dup2"}' +check "Duplicate time/employee" 400 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":2,"employeeId":7,"serviceId":1,"appointmentDate":"2027-09-01","appointmentTime":"09:00","appointmentStatus":"Scheduled"}' check "GET /appointments (customer)" 200 GET "/appointments" "${C[@]}" check "GET /appointments (staff)" 200 GET "/appointments" "${S[@]}" -check "Create appointment (no auth)" 401 POST "/appointments" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":1,"employeeId":4,"serviceId":1,"appointmentDate":"2027-10-01","appointmentTime":"10:00"}' +check "Create appointment (no auth)" 401 POST "/appointments" "${J[@]}" -d '{"petId":53,"customerId":32,"storeId":1,"employeeId":4,"serviceId":1,"appointmentDate":"2027-10-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}' echo "--- 9. ADOPTIONS ---" @@ -315,52 +315,52 @@ AVAIL_PET_1="" AVAIL_PET_2="" AVAIL_PET_3="" curl -s "$BASE/pets?status=Available&size=5" "${A[@]}" -o /tmp/qa_avail.json 2>/dev/null -AVAIL_PET_1=$(jq -r '.content[0].id // empty' /tmp/qa_avail.json 2>/dev/null) -AVAIL_PET_2=$(jq -r '.content[1].id // empty' /tmp/qa_avail.json 2>/dev/null) -AVAIL_PET_3=$(jq -r '.content[2].id // empty' /tmp/qa_avail.json 2>/dev/null) +AVAIL_PET_1=$(jq -r '.content[0].petId // empty' /tmp/qa_avail.json 2>/dev/null) +AVAIL_PET_2=$(jq -r '.content[1].petId // empty' /tmp/qa_avail.json 2>/dev/null) +AVAIL_PET_3=$(jq -r '.content[2].petId // empty' /tmp/qa_avail.json 2>/dev/null) if [ -n "$AVAIL_PET_1" ]; then - check "Staff create adoption (Pending)" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_1,\"customerId\":32,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST adopt1\"}" + check "Staff create adoption (Pending)" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_1,\"customerId\":32,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\",\"adoptionStatus\":\"Pending\"}" ADOPT_ID_1=$(id_from_body) [ -n "$ADOPT_ID_1" ] && CLEANUP_ADOPTION_IDS+=("$ADOPT_ID_1") check "Pet now Pending" 200 GET "/pets/$AVAIL_PET_1" - check_field "Pet status Pending" ".status" "Pending" + check_field "Pet status Pending" ".petStatus" "Pending" if [ -n "$ADOPT_ID_1" ]; then - check "Cancel adoption" 200 PUT "/adoptions/$ADOPT_ID_1" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_1,\"customerId\":32,\"storeId\":1,\"status\":\"Cancelled\",\"notes\":\"QA_TEST cancelled\"}" + check "Cancel adoption" 200 PUT "/adoptions/$ADOPT_ID_1" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_1,\"customerId\":32,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\",\"adoptionStatus\":\"Cancelled\"}" check "Pet back to Available" 200 GET "/pets/$AVAIL_PET_1" - check_field "Pet status Available" ".status" "Available" + check_field "Pet status Available" ".petStatus" "Available" fi fi if [ -n "$AVAIL_PET_2" ]; then - check "Staff create adoption 2" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_2,\"customerId\":33,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST adopt2\"}" + check "Staff create adoption 2" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_2,\"customerId\":33,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\",\"adoptionStatus\":\"Pending\"}" ADOPT_ID_2=$(id_from_body) [ -n "$ADOPT_ID_2" ] && CLEANUP_ADOPTION_IDS+=("$ADOPT_ID_2") if [ -n "$ADOPT_ID_2" ]; then - check "Complete adoption" 200 PUT "/adoptions/$ADOPT_ID_2" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_2,\"customerId\":33,\"storeId\":1,\"status\":\"Completed\",\"notes\":\"QA_TEST completed\"}" + check "Complete adoption" 200 PUT "/adoptions/$ADOPT_ID_2" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_2,\"customerId\":33,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\",\"adoptionStatus\":\"Completed\"}" check "Pet now Adopted" 200 GET "/pets/$AVAIL_PET_2" - check_field "Pet status Adopted" ".status" "Adopted" + check_field "Pet status Adopted" ".petStatus" "Adopted" fi fi if [ -n "$AVAIL_PET_3" ]; then - check "Customer request adoption" 201 POST "/adoptions" "${C[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"storeId\":1,\"notes\":\"QA_TEST customer adopt\"}" + check "Customer request adoption" 201 POST "/adoptions" "${C[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\"}" ADOPT_ID_3=$(id_from_body) [ -n "$ADOPT_ID_3" ] && CLEANUP_ADOPTION_IDS+=("$ADOPT_ID_3") if [ -n "$ADOPT_ID_3" ]; then - check "Cancel customer adoption" 200 PUT "/adoptions/$ADOPT_ID_3" "${C[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"storeId\":1,\"status\":\"Cancelled\",\"notes\":\"QA_TEST cust cancelled\"}" + check "Cancel customer adoption" 200 PUT "/adoptions/$ADOPT_ID_3" "${C[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\",\"adoptionStatus\":\"Cancelled\"}" check "Pet Available again" 200 GET "/pets/$AVAIL_PET_3" - check_field "Pet Available after cancel" ".status" "Available" + check_field "Pet Available after cancel" ".petStatus" "Available" fi - check "Adopt already pending" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"customerId\":34,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST pending block\"}" + check "Adopt already pending" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"customerId\":34,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\",\"adoptionStatus\":\"Pending\"}" ADOPT_BLOCK_ID=$(id_from_body) [ -n "$ADOPT_BLOCK_ID" ] && CLEANUP_ADOPTION_IDS+=("$ADOPT_BLOCK_ID") - check "Duplicate adoption for pending pet" 400 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"customerId\":35,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST dup\"}" + check "Duplicate adoption for pending pet" 400 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$AVAIL_PET_3,\"customerId\":35,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\",\"adoptionStatus\":\"Pending\"}" fi check "GET /adoptions (staff)" 200 GET "/adoptions" "${S[@]}" @@ -375,15 +375,15 @@ check "GET /sales (customer)" 403 GET "/sales" "${C[@]}" check "GET /sales/999999" 404 GET "/sales/999999" "${A[@]}" curl -s "$BASE/inventory?storeId=1" "${A[@]}" -o /tmp/qa_inv_before.json 2>/dev/null -INV_BEFORE=$(jq -r '.content[] | select(.productId == 1) | .quantity // 0' /tmp/qa_inv_before.json 2>/dev/null | head -1) +INV_BEFORE=$(jq -r '.content[] | select(.prodId == 1) | .quantity // 0' /tmp/qa_inv_before.json 2>/dev/null | head -1) -check "Staff create sale" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":1,"quantity":1}],"notes":"QA_TEST sale"}' +check "Staff create sale" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"prodId":1,"quantity":1}]}' SALE_ID=$(id_from_body) [ -n "$SALE_ID" ] && CLEANUP_SALE_IDS+=("$SALE_ID") if [ -n "$INV_BEFORE" ] && [ "$INV_BEFORE" != "" ] && [ "$INV_BEFORE" != "null" ]; then curl -s "$BASE/inventory?storeId=1" "${A[@]}" -o /tmp/qa_inv_after.json 2>/dev/null - INV_AFTER=$(jq -r '.content[] | select(.productId == 1) | .quantity // 0' /tmp/qa_inv_after.json 2>/dev/null | head -1) + INV_AFTER=$(jq -r '.content[] | select(.prodId == 1) | .quantity // 0' /tmp/qa_inv_after.json 2>/dev/null | head -1) if [ -n "$INV_AFTER" ] && [ "$INV_AFTER" != "null" ] && [ "$((INV_BEFORE - 1))" = "$INV_AFTER" ]; then PASS=$((PASS+1)) else @@ -391,12 +391,12 @@ if [ -n "$INV_BEFORE" ] && [ "$INV_BEFORE" != "" ] && [ "$INV_BEFORE" != "null" fi fi -check "Sale non-existent product" 404 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":999999,"quantity":1}]}' -check "Sale zero quantity" 400 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":1,"quantity":0}]}' -check "Sale (no auth)" 401 POST "/sales" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":1,"quantity":1}]}' -check "Sale (customer)" 403 POST "/sales" "${C[@]}" "${J[@]}" -d '{"storeId":1,"customerId":15,"items":[{"productId":1,"quantity":1}]}' +check "Sale non-existent product" 404 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"prodId":999999,"quantity":1}]}' +check "Sale zero quantity" 400 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"prodId":1,"quantity":0}]}' +check "Sale (no auth)" 401 POST "/sales" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"prodId":1,"quantity":1}]}' +check "Sale (customer)" 403 POST "/sales" "${C[@]}" "${J[@]}" -d '{"storeId":1,"customerId":15,"items":[{"prodId":1,"quantity":1}]}' -check "Staff create sale 2" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":2,"quantity":1}],"notes":"QA_TEST sale2"}' +check "Staff create sale 2" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"prodId":2,"quantity":1}]}' SALE_ID_2=$(id_from_body) [ -n "$SALE_ID_2" ] && CLEANUP_SALE_IDS+=("$SALE_ID_2") @@ -407,46 +407,46 @@ echo "--- 11. CART ---" check "GET /cart (customer)" 200 GET "/cart?storeId=1" "${C[@]}" -check "Add cart item" 200 POST "/cart/items" "${C[@]}" "${J[@]}" -d '{"productId":1,"storeId":1,"quantity":1}' -if [ "$(jq -r '.status // empty' /tmp/qa_body.json 2>/dev/null)" = "" ]; then - check "Add cart item alt" 201 POST "/cart/items" "${C[@]}" "${J[@]}" -d '{"productId":2,"storeId":1,"quantity":1}' +check "Add cart item" 200 POST "/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":1}' +if [ "$(jq -r '.cartStatus // empty' /tmp/qa_body.json 2>/dev/null)" = "" ]; then + check "Add cart item alt" 200 POST "/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":2,"storeId":1,"quantity":1}' fi check "GET cart with item" 200 GET "/cart?storeId=1" "${C[@]}" -CART_SUBTOTAL=$(jq -r '.subtotal // .totalBeforeDiscount // 0' /tmp/qa_body.json 2>/dev/null) -CART_ITEM_ID=$(jq -r '.items[0].id // .items[0].cartItemId // empty' /tmp/qa_body.json 2>/dev/null) +CART_SUBTOTAL=$(jq -r '.subtotalAmount // 0' /tmp/qa_body.json 2>/dev/null) +CART_ITEM_ID=$(jq -r '.items[0].cartItemId // empty' /tmp/qa_body.json 2>/dev/null) if [ -n "$CART_ITEM_ID" ]; then - check "Update cart qty" 200 PUT "/cart/items/$CART_ITEM_ID" "${C[@]}" "${J[@]}" -d '{"quantity":2}' + check "Update cart qty" 200 PUT "/cart/update" "${C[@]}" "${J[@]}" -d "{\"cartItemId\":$CART_ITEM_ID,\"quantity\":2}" check "GET cart after qty update" 200 GET "/cart?storeId=1" "${C[@]}" fi -check "Apply coupon WELCOME10" 200 POST "/cart/coupon" "${C[@]}" "${J[@]}" -d '{"couponCode":"WELCOME10","storeId":1}' +check "Apply coupon WELCOME10" 200 POST "/cart/apply-coupon?storeId=1" "${C[@]}" "${J[@]}" -d '{"couponCode":"WELCOME10"}' check "GET cart with coupon" 200 GET "/cart?storeId=1" "${C[@]}" -DISCOUNT=$(jq -r '.discount // .totalDiscount // 0' /tmp/qa_body.json 2>/dev/null) +DISCOUNT=$(jq -r '.discountAmount // 0' /tmp/qa_body.json 2>/dev/null) -check "Remove coupon" 200 DELETE "/cart/coupon?storeId=1" "${C[@]}" +check "Remove coupon" 200 DELETE "/cart/coupon?storeId=1" "${C[@]}" "${J[@]}" check "GET cart no coupon" 200 GET "/cart?storeId=1" "${C[@]}" -check "Apply points" 200 POST "/cart/points" "${C[@]}" "${J[@]}" -d '{"points":10,"storeId":1}' -check "Remove points" 200 DELETE "/cart/points?storeId=1" "${C[@]}" +check "Apply points" 200 POST "/cart/apply-points?storeId=1&useLoyaltyPoints=true" "${C[@]}" "${J[@]}" +check "Remove points" 200 POST "/cart/apply-points?storeId=1&useLoyaltyPoints=false" "${C[@]}" "${J[@]}" if [ -n "$CART_ITEM_ID" ]; then - check "Remove cart item" 200 DELETE "/cart/items/$CART_ITEM_ID" "${C[@]}" + check "Remove cart item" 200 DELETE "/cart/remove/$CART_ITEM_ID" "${C[@]}" fi -check "Clear cart" 200 DELETE "/cart?storeId=1" "${C[@]}" +check "Clear cart" 204 DELETE "/cart/clear?storeId=1" "${C[@]}" -check "Add item qty 0" 400 POST "/cart/items" "${C[@]}" "${J[@]}" -d '{"productId":1,"storeId":1,"quantity":0}' -check "Apply invalid coupon" 400 POST "/cart/coupon" "${C[@]}" "${J[@]}" -d '{"couponCode":"FAKECOUPON999","storeId":1}' +check "Add item qty 0" 400 POST "/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":0}' +check "Apply invalid coupon" 400 POST "/cart/apply-coupon?storeId=1" "${C[@]}" "${J[@]}" -d '{"couponCode":"FAKECOUPON999"}' check "Checkout empty cart" 400 POST "/cart/checkout" "${C[@]}" "${J[@]}" -d '{"storeId":1}' -check "Add item for checkout test" 200 POST "/cart/items" "${C[@]}" "${J[@]}" -d '{"productId":1,"storeId":1,"quantity":1}' +check "Add item for checkout test" 200 POST "/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":1}' check "Checkout" 200 POST "/cart/checkout" "${C[@]}" "${J[@]}" -d '{"storeId":1}' -CHECKOUT_SALE_ID=$(jq -r '.id // .saleId // empty' /tmp/qa_body.json 2>/dev/null) +CHECKOUT_SALE_ID=$(jq -r '.cartId // empty' /tmp/qa_body.json 2>/dev/null) [ -n "$CHECKOUT_SALE_ID" ] && CLEANUP_SALE_IDS+=("$CHECKOUT_SALE_ID") -check "Clear cart final" 200 DELETE "/cart?storeId=1" "${C[@]}" +check "Clear cart final" 204 DELETE "/cart/clear?storeId=1" "${C[@]}" echo "--- 12. REFUNDS ---" @@ -458,21 +458,21 @@ if [ -n "$CHECKOUT_SALE_ID" ]; then CUST_SALE_ID="$CHECKOUT_SALE_ID" else curl -s "$BASE/sales/my" "${C[@]}" -o /tmp/qa_my_sales.json 2>/dev/null - CUST_SALE_ID=$(jq -r '.content[0].id // .[0].id // empty' /tmp/qa_my_sales.json 2>/dev/null) + CUST_SALE_ID=$(jq -r '.content[0].saleId // .[0].saleId // empty' /tmp/qa_my_sales.json 2>/dev/null) fi if [ -n "$CUST_SALE_ID" ]; then - check "Customer create refund" 201 POST "/refunds" "${C[@]}" "${J[@]}" -d "{\"saleId\":$CUST_SALE_ID,\"reason\":\"QA_TEST refund reason\"}" + check "Customer create refund" 201 POST "/refunds" "${C[@]}" "${J[@]}" -d "{\"saleId\":$CUST_SALE_ID,\"reason\":\"QA_TEST refund reason\",\"items\":[{\"prodId\":1,\"quantity\":1}]}" REFUND_ID=$(id_from_body) if [ -n "$REFUND_ID" ]; then - check "Staff approve refund" 200 PUT "/refunds/$REFUND_ID" "${S[@]}" "${J[@]}" -d "{\"status\":\"Approved\",\"saleId\":$CUST_SALE_ID,\"reason\":\"QA_TEST approved\"}" + check "Staff approve refund" 200 PUT "/refunds/$REFUND_ID" "${S[@]}" "${J[@]}" -d '{"status":"Approved"}' check_field "Refund approved" ".status" "Approved" fi fi -check "Refund non-existent sale" 404 POST "/refunds" "${C[@]}" "${J[@]}" -d '{"saleId":999999,"reason":"QA_TEST nope"}' -check "Refund missing reason" 400 POST "/refunds" "${C[@]}" "${J[@]}" -d '{"saleId":1}' +check "Refund non-existent sale" 404 POST "/refunds" "${C[@]}" "${J[@]}" -d '{"saleId":999999,"reason":"QA_TEST nope","items":[{"prodId":1,"quantity":1}]}' +check "Refund missing reason" 400 POST "/refunds" "${C[@]}" "${J[@]}" -d '{"saleId":1,"items":[{"prodId":1,"quantity":1}]}' check "GET /refunds (staff)" 200 GET "/refunds" "${S[@]}" check "GET /refunds paginated" 200 GET "/refunds?page=0&size=5" "${A[@]}" @@ -483,30 +483,30 @@ check "GET /coupons/1" 200 GET "/coupons/1" "${A[@]}" check "GET /coupons/code/WELCOME10" 200 GET "/coupons/code/WELCOME10" "${A[@]}" check "GET /coupons (customer)" 403 GET "/coupons" "${C[@]}" -check "Create coupon" 201 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"QA_TEST_CPN","discountPercent":10,"description":"QA test coupon","expiryDate":"2028-12-31","maxUses":100}' +check "Create coupon" 201 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"QA_TEST_CPN","discountType":"PERCENTAGE","discountValue":10,"usageLimit":100}' CPN_ID=$(id_from_body) [ -n "$CPN_ID" ] && CLEANUP_COUPON_IDS+=("$CPN_ID") -check_field "Coupon code" ".code" "QA_TEST_CPN" +check_field "Coupon code" ".couponCode" "QA_TEST_CPN" if [ -n "$CPN_ID" ]; then - check "Update coupon" 200 PUT "/coupons/$CPN_ID" "${A[@]}" "${J[@]}" -d "{\"code\":\"QA_TEST_CPN\",\"discountPercent\":15,\"description\":\"updated\",\"expiryDate\":\"2028-12-31\",\"maxUses\":50}" + check "Update coupon" 200 PUT "/coupons/$CPN_ID" "${A[@]}" "${J[@]}" -d "{\"couponCode\":\"QA_TEST_CPN\",\"discountType\":\"PERCENTAGE\",\"discountValue\":15,\"usageLimit\":50}" fi -check "Create coupon to delete" 201 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"QA_TEST_DEL","discountPercent":5,"description":"del","expiryDate":"2028-12-31","maxUses":10}' +check "Create coupon to delete" 201 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"QA_TEST_DEL","discountType":"PERCENTAGE","discountValue":5,"usageLimit":10}' DEL_CPN_ID=$(id_from_body) if [ -n "$DEL_CPN_ID" ]; then check "Delete coupon" 200 DELETE "/coupons/$DEL_CPN_ID" "${A[@]}" fi -check "Coupon 100% discount" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"QA_TEST_100","discountPercent":100,"description":"bad","expiryDate":"2028-12-31","maxUses":1}' -check "Coupon 150% discount" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"QA_TEST_150","discountPercent":150,"description":"bad","expiryDate":"2028-12-31","maxUses":1}' +check "Coupon 100% discount" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"QA_TEST_100","discountType":"PERCENTAGE","discountValue":100,"usageLimit":1}' +check "Coupon 150% discount" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"QA_TEST_150","discountType":"PERCENTAGE","discountValue":150,"usageLimit":1}' -check "Coupon 99.99%" 201 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"QA_TEST_99","discountPercent":99.99,"description":"boundary","expiryDate":"2028-12-31","maxUses":1}' +check "Coupon 99.99%" 201 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"QA_TEST_99","discountType":"PERCENTAGE","discountValue":99.99,"usageLimit":1}' BOUNDARY_CPN_ID=$(id_from_body) [ -n "$BOUNDARY_CPN_ID" ] && CLEANUP_COUPON_IDS+=("$BOUNDARY_CPN_ID") -check "Duplicate coupon code" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"code":"WELCOME10","discountPercent":5,"description":"dup","expiryDate":"2028-12-31","maxUses":1}' -check "Create coupon (customer)" 403 POST "/coupons" "${C[@]}" "${J[@]}" -d '{"code":"QA_TEST_NOPE","discountPercent":5,"description":"nope","expiryDate":"2028-12-31","maxUses":1}' +check "Duplicate coupon code" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"WELCOME10","discountType":"PERCENTAGE","discountValue":5,"usageLimit":1}' +check "Create coupon (customer)" 403 POST "/coupons" "${C[@]}" "${J[@]}" -d '{"couponCode":"QA_TEST_NOPE","discountType":"PERCENTAGE","discountValue":5,"usageLimit":1}' echo "--- 14. SUPPLIERS / PRODUCT-SUPPLIERS / INVENTORY / PURCHASE-ORDERS ---" @@ -514,15 +514,15 @@ check "GET /suppliers (admin)" 200 GET "/suppliers" "${A[@]}" check "GET /suppliers/1" 200 GET "/suppliers/1" "${A[@]}" check "GET /suppliers (customer)" 403 GET "/suppliers" "${C[@]}" -check "Create supplier" 201 POST "/suppliers" "${A[@]}" "${J[@]}" -d '{"supplierName":"QA_TEST_Supplier","contactName":"QA Contact","email":"qa_supplier@test.com","phone":"5553333333","address":"100 Supply Rd"}' +check "Create supplier" 201 POST "/suppliers" "${A[@]}" "${J[@]}" -d '{"supCompany":"QA_TEST_Supplier","supContactFirstName":"QA","supContactLastName":"Contact","supEmail":"qa_supplier@test.com","supPhone":"5553333333"}' SUPP_ID=$(id_from_body) [ -n "$SUPP_ID" ] && CLEANUP_SUPPLIER_IDS+=("$SUPP_ID") if [ -n "$SUPP_ID" ]; then - check "Update supplier" 200 PUT "/suppliers/$SUPP_ID" "${A[@]}" "${J[@]}" -d "{\"supplierName\":\"QA_TEST_SuppUpd\",\"contactName\":\"QA Updated\",\"email\":\"qa_supp_upd@test.com\",\"phone\":\"5553333334\",\"address\":\"101 Supply Rd\"}" + check "Update supplier" 200 PUT "/suppliers/$SUPP_ID" "${A[@]}" "${J[@]}" -d "{\"supCompany\":\"QA_TEST_SuppUpd\",\"supContactFirstName\":\"QA\",\"supContactLastName\":\"Updated\",\"supEmail\":\"qa_supp_upd@test.com\",\"supPhone\":\"5553333334\"}" fi -check "Create supplier to delete" 201 POST "/suppliers" "${A[@]}" "${J[@]}" -d '{"supplierName":"QA_TEST_SuppDel","contactName":"Del","email":"del@test.com","phone":"5550000000","address":"x"}' +check "Create supplier to delete" 201 POST "/suppliers" "${A[@]}" "${J[@]}" -d '{"supCompany":"QA_TEST_SuppDel","supContactFirstName":"Del","supContactLastName":"Test","supEmail":"del@test.com","supPhone":"5550000000"}' DEL_SUPP_ID=$(id_from_body) if [ -n "$DEL_SUPP_ID" ]; then check "Delete supplier" 200 DELETE "/suppliers/$DEL_SUPP_ID" "${A[@]}" @@ -551,7 +551,7 @@ if [ -n "$CONV_ID" ]; then check "GET messages" 200 GET "/chat/conversations/$CONV_ID/messages" "${C[@]}" - check "Request human takeover" 200 POST "/chat/conversations/$CONV_ID/takeover" "${C[@]}" "${J[@]}" -d '{}' + check "Request human takeover" 200 POST "/chat/conversations/$CONV_ID/request-human" "${C[@]}" "${J[@]}" -d '{}' check "Staff view conversations" 200 GET "/chat/conversations" "${S[@]}" @@ -559,7 +559,7 @@ if [ -n "$CONV_ID" ]; then check "GET conversation detail" 200 GET "/chat/conversations/$CONV_ID" "${S[@]}" - check "Close conversation" 200 PUT "/chat/conversations/$CONV_ID/close" "${S[@]}" "${J[@]}" -d '{}' + check "Close conversation" 200 PUT "/chat/conversations/$CONV_ID" "${S[@]}" "${J[@]}" -d '{"status":"CLOSED"}' check "GET closed conversation" 200 GET "/chat/conversations/$CONV_ID" "${C[@]}" check_field "Conversation closed" ".status" "CLOSED" @@ -594,15 +594,15 @@ check "GET /dropdowns/customers/32/pets" 200 GET "/dropdowns/customers/32/pets" echo "--- 17. CROSS-ENTITY ---" curl -s "$BASE/inventory?storeId=1" "${A[@]}" -o /tmp/qa_cross_inv1.json 2>/dev/null -CROSS_INV_BEFORE=$(jq -r '.content[] | select(.productId == 2) | .quantity // 0' /tmp/qa_cross_inv1.json 2>/dev/null | head -1) +CROSS_INV_BEFORE=$(jq -r '.content[] | select(.prodId == 2) | .quantity // 0' /tmp/qa_cross_inv1.json 2>/dev/null | head -1) -check "Cross: create sale" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"productId":2,"quantity":1}],"notes":"QA_TEST_CROSS"}' +check "Cross: create sale" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"customerId":32,"items":[{"prodId":2,"quantity":1}]}' CROSS_SALE_ID=$(id_from_body) [ -n "$CROSS_SALE_ID" ] && CLEANUP_SALE_IDS+=("$CROSS_SALE_ID") if [ -n "$CROSS_INV_BEFORE" ] && [ "$CROSS_INV_BEFORE" != "null" ] && [ "$CROSS_INV_BEFORE" != "" ]; then curl -s "$BASE/inventory?storeId=1" "${A[@]}" -o /tmp/qa_cross_inv2.json 2>/dev/null - CROSS_INV_AFTER=$(jq -r '.content[] | select(.productId == 2) | .quantity // 0' /tmp/qa_cross_inv2.json 2>/dev/null | head -1) + CROSS_INV_AFTER=$(jq -r '.content[] | select(.prodId == 2) | .quantity // 0' /tmp/qa_cross_inv2.json 2>/dev/null | head -1) if [ "$((CROSS_INV_BEFORE - 1))" = "$CROSS_INV_AFTER" ]; then PASS=$((PASS+1)) else @@ -611,12 +611,12 @@ if [ -n "$CROSS_INV_BEFORE" ] && [ "$CROSS_INV_BEFORE" != "null" ] && [ "$CROSS_ fi if [ -n "$CROSS_SALE_ID" ]; then - check "Cross: create refund" 201 POST "/refunds" "${C[@]}" "${J[@]}" -d "{\"saleId\":$CROSS_SALE_ID,\"reason\":\"QA_TEST cross refund\"}" + check "Cross: create refund" 201 POST "/refunds" "${C[@]}" "${J[@]}" -d "{\"saleId\":$CROSS_SALE_ID,\"reason\":\"QA_TEST cross refund\",\"items\":[{\"prodId\":2,\"quantity\":1}]}" CROSS_REFUND_ID=$(id_from_body) if [ -n "$CROSS_REFUND_ID" ]; then - check "Cross: approve refund" 200 PUT "/refunds/$CROSS_REFUND_ID" "${S[@]}" "${J[@]}" -d "{\"status\":\"Approved\",\"saleId\":$CROSS_SALE_ID,\"reason\":\"QA_TEST cross approved\"}" + check "Cross: approve refund" 200 PUT "/refunds/$CROSS_REFUND_ID" "${S[@]}" "${J[@]}" -d '{"status":"Approved"}' curl -s "$BASE/inventory?storeId=1" "${A[@]}" -o /tmp/qa_cross_inv3.json 2>/dev/null - CROSS_INV_RESTORED=$(jq -r '.content[] | select(.productId == 2) | .quantity // 0' /tmp/qa_cross_inv3.json 2>/dev/null | head -1) + CROSS_INV_RESTORED=$(jq -r '.content[] | select(.prodId == 2) | .quantity // 0' /tmp/qa_cross_inv3.json 2>/dev/null | head -1) if [ -n "$CROSS_INV_RESTORED" ] && [ "$CROSS_INV_RESTORED" = "$CROSS_INV_BEFORE" ]; then PASS=$((PASS+1)) else @@ -626,35 +626,35 @@ if [ -n "$CROSS_SALE_ID" ]; then fi curl -s "$BASE/pets?status=Available&size=5&page=1" "${A[@]}" -o /tmp/qa_cross_avail.json 2>/dev/null -CROSS_PET=$(jq -r '.content[0].id // empty' /tmp/qa_cross_avail.json 2>/dev/null) +CROSS_PET=$(jq -r '.content[0].petId // empty' /tmp/qa_cross_avail.json 2>/dev/null) if [ -n "$CROSS_PET" ]; then - check "Cross: create adoption Pending" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET,\"customerId\":35,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST cross adopt\"}" + check "Cross: create adoption Pending" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET,\"customerId\":35,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\",\"adoptionStatus\":\"Pending\"}" CROSS_ADOPT_ID=$(id_from_body) [ -n "$CROSS_ADOPT_ID" ] && CLEANUP_ADOPTION_IDS+=("$CROSS_ADOPT_ID") check "Cross: pet is Pending" 200 GET "/pets/$CROSS_PET" - check_field "Cross pet Pending" ".status" "Pending" + check_field "Cross pet Pending" ".petStatus" "Pending" if [ -n "$CROSS_ADOPT_ID" ]; then - check "Cross: complete adoption" 200 PUT "/adoptions/$CROSS_ADOPT_ID" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET,\"customerId\":35,\"storeId\":1,\"status\":\"Completed\",\"notes\":\"QA_TEST cross completed\"}" + check "Cross: complete adoption" 200 PUT "/adoptions/$CROSS_ADOPT_ID" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET,\"customerId\":35,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\",\"adoptionStatus\":\"Completed\"}" check "Cross: pet is Adopted" 200 GET "/pets/$CROSS_PET" - check_field "Cross pet Adopted" ".status" "Adopted" + check_field "Cross pet Adopted" ".petStatus" "Adopted" fi fi curl -s "$BASE/pets?status=Available&size=5&page=2" "${A[@]}" -o /tmp/qa_cross_avail2.json 2>/dev/null -CROSS_PET_2=$(jq -r '.content[0].id // empty' /tmp/qa_cross_avail2.json 2>/dev/null) +CROSS_PET_2=$(jq -r '.content[0].petId // empty' /tmp/qa_cross_avail2.json 2>/dev/null) if [ -n "$CROSS_PET_2" ]; then - check "Cross: adopt then cancel" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET_2,\"customerId\":36,\"storeId\":1,\"status\":\"Pending\",\"notes\":\"QA_TEST cross cancel\"}" + check "Cross: adopt then cancel" 201 POST "/adoptions" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET_2,\"customerId\":36,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\",\"adoptionStatus\":\"Pending\"}" CROSS_ADOPT_ID_2=$(id_from_body) [ -n "$CROSS_ADOPT_ID_2" ] && CLEANUP_ADOPTION_IDS+=("$CROSS_ADOPT_ID_2") if [ -n "$CROSS_ADOPT_ID_2" ]; then - check "Cross: cancel adoption" 200 PUT "/adoptions/$CROSS_ADOPT_ID_2" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET_2,\"customerId\":36,\"storeId\":1,\"status\":\"Cancelled\",\"notes\":\"QA_TEST cross cancelled\"}" + check "Cross: cancel adoption" 200 PUT "/adoptions/$CROSS_ADOPT_ID_2" "${S[@]}" "${J[@]}" -d "{\"petId\":$CROSS_PET_2,\"customerId\":36,\"sourceStoreId\":1,\"adoptionDate\":\"2027-06-15\",\"adoptionStatus\":\"Cancelled\"}" check "Cross: pet Available" 200 GET "/pets/$CROSS_PET_2" - check_field "Cross pet Available" ".status" "Available" + check_field "Cross pet Available" ".petStatus" "Available" fi fi @@ -662,7 +662,7 @@ echo "" echo "--- CLEANUP ---" for id in "${CLEANUP_CONV_IDS[@]}"; do - curl -s -o /dev/null -X PUT "$BASE/chat/conversations/$id/close" "${S[@]}" "${J[@]}" -d '{}' 2>/dev/null + curl -s -o /dev/null -X PUT "$BASE/chat/conversations/$id" "${S[@]}" "${J[@]}" -d '{"status":"CLOSED"}' 2>/dev/null done for id in "${CLEANUP_ADOPTION_IDS[@]}"; do -- 2.49.1 From 8873ef5bef9a10d39d0ba6ed06a6c6a3713511f9 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 07:55:17 -0600 Subject: [PATCH 25/34] add CORRECT test script --- test-backend-correct.sh | 512 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100755 test-backend-correct.sh diff --git a/test-backend-correct.sh b/test-backend-correct.sh new file mode 100755 index 00000000..406638a3 --- /dev/null +++ b/test-backend-correct.sh @@ -0,0 +1,512 @@ +#!/usr/bin/env bash +set -uo pipefail + +BASE="https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/api/v1" +PASS=0 FAIL=0 +CLEANUP_PRODUCT_IDS=() +CLEANUP_SALE_IDS=() +CLEANUP_COUPON_IDS=() + +check() { + local label="$1" expect="$2" method="$3" path="$4"; shift 4 + local code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -X "$method" "$BASE$path" "$@" 2>/dev/null) + if [ "$code" = "$expect" ]; then PASS=$((PASS+1)) + else FAIL=$((FAIL+1)); echo "FAIL: $label — expected $expect got $code"; fi +} +check_field() { + local actual=$(jq -r "$2" /tmp/qa_body.json 2>/dev/null) + if [ "$actual" = "$3" ]; then PASS=$((PASS+1)) + else FAIL=$((FAIL+1)); echo "FAIL: $1 — expected '$3' got '$actual'"; fi +} +check_not_500() { + local label="$1" method="$2" path="$3"; shift 3 + local code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -X "$method" "$BASE$path" "$@" 2>/dev/null) + if [ "$code" != "500" ]; then PASS=$((PASS+1)) + else FAIL=$((FAIL+1)); echo "FAIL: $label — got 500 Internal Server Error"; fi +} +check_true() { + local label="$1" expr="$2" + local result=$(jq "$expr" /tmp/qa_body.json 2>/dev/null) + if [ "$result" = "true" ]; then PASS=$((PASS+1)) + else FAIL=$((FAIL+1)); echo "FAIL: $label"; fi +} + +echo "=== Logging in ===" +ADMIN_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' | jq -r '.token') +STAFF_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"staff","password":"staff123"}' | jq -r '.token') +CUST_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"customer","password":"customer123"}' | jq -r '.token') + +if [ "$ADMIN_TOKEN" = "null" ] || [ -z "$ADMIN_TOKEN" ]; then echo "FATAL: Admin login failed"; exit 1; fi +if [ "$STAFF_TOKEN" = "null" ] || [ -z "$STAFF_TOKEN" ]; then echo "FATAL: Staff login failed"; exit 1; fi +if [ "$CUST_TOKEN" = "null" ] || [ -z "$CUST_TOKEN" ]; then echo "FATAL: Customer login failed"; exit 1; fi +echo "All 3 logins OK" + +A=(-H "Authorization: Bearer $ADMIN_TOKEN") +S=(-H "Authorization: Bearer $STAFF_TOKEN") +C=(-H "Authorization: Bearer $CUST_TOKEN") +J=(-H "Content-Type: application/json") + +cleanup() { + echo "" + echo "=== Cleanup ===" + for pid in "${CLEANUP_PRODUCT_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/products/$pid" "${A[@]}" 2>/dev/null + done + for sid in "${CLEANUP_SALE_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/sales/$sid" "${A[@]}" 2>/dev/null + done + for cid in "${CLEANUP_COUPON_IDS[@]}"; do + curl -s -o /dev/null -X DELETE "$BASE/coupons/$cid" "${A[@]}" 2>/dev/null + done + echo "Cleanup done" +} +trap cleanup EXIT + +############################################################################### +echo "" +echo "========== C — CONFORMANCE ==========" +############################################################################### + +echo "--- Response format validation ---" +check "GET /products 200" 200 GET "/products" +check_true "products .content is array" '.content | type == "array"' +check_true "products has totalElements" '.totalElements != null' +check_true "products has totalPages" '.totalPages != null' +check_true "products has number" '.number != null' +check_true "products has size" '.size != null' + +check "GET /products/1 200" 200 GET "/products/1" +check_true "prodPrice is number ≤2 decimals" '(.prodPrice | tostring | test("^[0-9]+(\\.[0-9]{1,2})?$"))' + +check "GET /sales admin 200" 200 GET "/sales" "${A[@]}" "${J[@]}" +check_true "saleDate has ISO T" '(.content[0].saleDate | test("T"))' + +check "GET /appointments admin 200" 200 GET "/appointments" "${A[@]}" "${J[@]}" +check_true "appointmentDate YYYY-MM-DD" '(.content[0].appointmentDate | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}"))' + +local_code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -D /tmp/qa_headers.txt -X GET "$BASE/auth/me" "${A[@]}" 2>/dev/null) +if grep -qi "application/json" /tmp/qa_headers.txt 2>/dev/null; then PASS=$((PASS+1)) +else FAIL=$((FAIL+1)); echo "FAIL: /auth/me Content-Type not json"; fi + +local_code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -D /tmp/qa_headers.txt -X GET "$BASE/users/1/avatar/file" "${A[@]}" 2>/dev/null) +if grep -qi "image/" /tmp/qa_headers.txt 2>/dev/null; then PASS=$((PASS+1)) +else FAIL=$((FAIL+1)); echo "FAIL: avatar Content-Type not image/"; fi + +echo "--- Status enum validation ---" +check "GET /appointments enum" 200 GET "/appointments?size=50" "${A[@]}" +check_true "appointmentStatus valid" '[.content[].appointmentStatus] | all(. == "Scheduled" or . == "Completed" or . == "Cancelled" or . == "In Progress" or . == "No Show")' + +check "GET /pets enum" 200 GET "/pets?size=100" +check_true "petStatus valid" '[.content[].petStatus] | all(. == "Available" or . == "Pending" or . == "Adopted" or . == "Owned")' + +check "GET /sales enum" 200 GET "/sales?size=50" "${A[@]}" +check_true "channel valid" '[.content[].channel] | all(. == "IN_STORE" or . == "ONLINE")' + +check "GET /users enum" 200 GET "/users?size=100" "${A[@]}" +check_true "role valid" '[.content[].role] | all(. == "ADMIN" or . == "STAFF" or . == "CUSTOMER")' + +check "GET /coupons enum" 200 GET "/coupons" "${A[@]}" +check_true "discountType valid" '[.[].discountType] | all(. == "PERCENTAGE" or . == "FIXED")' + +echo "--- Error format validation ---" +check "GET /products/999999 404" 404 GET "/products/999999" +check_true "404 has status" '.status != null' +check_true "404 has message" '.message != null' +check_true "404 has path" '.path != null' +check_true "404 has timestamp" '.timestamp != null' + +echo "--- Input format enforcement ---" +check_not_500 "invalid JSON body" POST "/products" "${A[@]}" "${J[@]}" -d '{bad json}' +check_not_500 "text/plain content-type" POST "/products" "${A[@]}" -H "Content-Type: text/plain" -d 'hello' +check_not_500 "empty body" POST "/products" "${A[@]}" "${J[@]}" -d '' +check_not_500 "XML body" POST "/products" "${A[@]}" -H "Content-Type: application/xml" -d '' + +echo "--- Security conformance ---" +check_not_500 "SQL injection in search" GET "/products?q=%27%20OR%201%3D1--" +check_not_500 "XSS in search" GET '/products?q=' + +check_not_500 "XSS in prodName" POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"","prodPrice":9.99,"categoryId":1}' +XSS_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null) +if [ -n "$XSS_ID" ]; then + CLEANUP_PRODUCT_IDS+=("$XSS_ID") + check "GET XSS product" 200 GET "/products/$XSS_ID" + check_true "XSS stored literally" '(.prodName | contains("/dev/null) +if [ -n "$UNI_ID" ]; then + CLEANUP_PRODUCT_IDS+=("$UNI_ID") + check "GET unicode product" 200 GET "/products/$UNI_ID" + check_field "unicode preserved" '.prodName' 'Café Résumé' +fi + +check_not_500 "null byte in category" POST "/categories" "${A[@]}" "${J[@]}" -d '{"categoryName":"test\u0000inject"}' + +############################################################################### +echo "" +echo "========== O — ORDERING ==========" +############################################################################### + +check "products price asc" 200 GET "/products?sort=prodPrice,asc&size=100" +check_true "prices non-decreasing" '[.content[].prodPrice] | . as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])' + +check "products price desc" 200 GET "/products?sort=prodPrice,desc&size=100" +check_true "prices non-increasing" '[.content[].prodPrice] | . as $a | [range(1;length)] | all(. as $i | $a[$i] <= $a[$i-1])' + +check "products name asc" 200 GET "/products?sort=prodName,asc&size=100" +check_true "names alphabetical" '[.content[].prodName] | . as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])' + +check "sales date desc" 200 GET "/sales?sort=saleDate,desc&size=50" "${A[@]}" +check_true "sale dates non-increasing" '[.content[].saleDate] | . as $a | [range(1;length)] | all(. as $i | $a[$i] <= $a[$i-1])' + +check "users lastName asc" 200 GET "/users?sort=lastName,asc&size=100" "${A[@]}" +check_true "lastNames alphabetical" '[.content[].lastName] | . as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])' + +check_not_500 "sort by nonexistent field" GET "/products?sort=nonexistent,asc" + +check "page 0 size 5" 200 GET "/products?page=0&size=5" +PAGE0_IDS=$(jq -r '[.content[].prodId] | join(",")' /tmp/qa_body.json) +check "page 1 size 5" 200 GET "/products?page=1&size=5" +PAGE1_IDS=$(jq -r '[.content[].prodId] | join(",")' /tmp/qa_body.json) +OVERLAP=0 +IFS=',' read -ra P0 <<< "$PAGE0_IDS" +IFS=',' read -ra P1 <<< "$PAGE1_IDS" +for a in "${P0[@]}"; do for b in "${P1[@]}"; do [ "$a" = "$b" ] && OVERLAP=1; done; done +if [ "$OVERLAP" = "0" ]; then PASS=$((PASS+1)) +else FAIL=$((FAIL+1)); echo "FAIL: page 0 and page 1 overlap"; fi + +check "conversations" 200 GET "/chat/conversations" "${C[@]}" +CONV_ID=$(jq -r '.[0].conversationId // empty' /tmp/qa_body.json 2>/dev/null) +if [ -n "$CONV_ID" ]; then + check "conv messages" 200 GET "/chat/conversations/$CONV_ID/messages" "${C[@]}" + check_true "message timestamps ascending" '. as $a | [range(1;length)] | all(. as $i | $a[$i].timestamp >= $a[$i-1].timestamp)' +fi + +check "activity logs desc" 200 GET "/activity-logs?limit=10" "${A[@]}" +check_true "log timestamps descending" '. as $a | [range(1;length)] | all(. as $i | $a[$i].logTimestamp <= $a[$i-1].logTimestamp)' + +check "availability times" 200 GET "/appointments/availability?storeId=1&serviceId=1&date=2027-06-01" "${A[@]}" +check_true "times ascending" '. as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])' + +############################################################################### +echo "" +echo "========== R — RANGE ==========" +############################################################################### + +echo "--- Data range checks ---" +check "products range" 200 GET "/products?size=200" +check_true "all prodPrice > 0" '[.content[].prodPrice] | all(. > 0)' + +check "services range" 200 GET "/services" +check_true "all servicePrice > 0" '[.[].servicePrice] | all(. > 0)' +check_true "all serviceDuration > 0" '[.[].serviceDuration] | all(. > 0)' + +check "inventory range" 200 GET "/inventory" "${A[@]}" +check_true "all quantity >= 0" '[.content[].quantity] | all(. >= 0)' + +check "users loyalty" 200 GET "/users?size=100" "${A[@]}" +check_true "all loyaltyPoints >= 0" '[.content[].loyaltyPoints] | all(. >= 0)' + +check "coupons range" 200 GET "/coupons" "${A[@]}" +check_true "PERCENTAGE < 100 and > 0" '[.[] | select(.discountType == "PERCENTAGE") | .discountValue] | all(. > 0 and . < 100)' + +check "pets age range" 200 GET "/pets?size=200" +check_true "all petAge >= 0" '[.content[].petAge | select(. != null)] | all(. >= 0)' + +echo "--- Input range enforcement ---" +check "negative price → 400" 400 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_NEG","prodPrice":-1,"categoryId":1}' +NEG_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null) +[ -n "$NEG_ID" ] && CLEANUP_PRODUCT_IDS+=("$NEG_ID") + +check "zero price → 400" 400 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_ZERO","prodPrice":0,"categoryId":1}' +Z_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null) +[ -n "$Z_ID" ] && CLEANUP_PRODUCT_IDS+=("$Z_ID") + +check "zero duration → 400" 400 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_DUR0","servicePrice":10,"serviceDuration":0}' +check "neg duration → 400" 400 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_DURN","servicePrice":10,"serviceDuration":-5}' + +check "neg cart qty → 400" 400 POST "/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":-1}' + +check "zero coupon pct → 400" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"QA_ZERO_PCT","discountType":"PERCENTAGE","discountValue":0}' + +check_not_500 "huge price" POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_HUGE","prodPrice":99999999.99,"categoryId":1}' +HUGE_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null) +[ -n "$HUGE_ID" ] && CLEANUP_PRODUCT_IDS+=("$HUGE_ID") + +echo "--- Extreme pagination ---" +check_not_500 "page=-1" GET "/products?page=-1" +check_not_500 "size=0" GET "/products?size=0" +check_not_500 "size=10000" GET "/products?size=10000" + +echo "--- String length ---" +LONG_USER=$(printf 'u%.0s' $(seq 1 60)) +check "60-char username → 400" 400 POST "/auth/register" "${J[@]}" -d "{\"username\":\"$LONG_USER\",\"password\":\"pass1234\",\"firstName\":\"Q\",\"lastName\":\"A\",\"email\":\"${LONG_USER}@test.com\"}" + +LONG_PNAME=$(printf 'p%.0s' $(seq 1 200)) +check "200-char prodName → 400" 400 POST "/products" "${A[@]}" "${J[@]}" -d "{\"prodName\":\"$LONG_PNAME\",\"prodPrice\":9.99,\"categoryId\":1}" + +############################################################################### +echo "" +echo "========== R — REFERENCE ==========" +############################################################################### + +echo "--- FK integrity: sales ---" +check "sales sample" 200 GET "/sales?size=3" "${A[@]}" +for i in 0 1 2; do + SID=$(jq -r ".content[$i].storeId // empty" /tmp/qa_body.json) + EID=$(jq -r ".content[$i].employeeId // empty" /tmp/qa_body.json) + [ -n "$SID" ] && check "sale[$i] store $SID exists" 200 GET "/stores/$SID" + [ -n "$EID" ] && check "sale[$i] employee $EID exists" 200 GET "/users/$EID" "${A[@]}" +done + +echo "--- FK integrity: appointments ---" +check "appts sample" 200 GET "/appointments?size=3" "${A[@]}" +cp /tmp/qa_body.json /tmp/qa_appts.json +for i in 0 1 2; do + SVID=$(jq -r ".content[$i].serviceId // empty" /tmp/qa_appts.json) + STID=$(jq -r ".content[$i].storeId // empty" /tmp/qa_appts.json) + CID=$(jq -r ".content[$i].customerId // empty" /tmp/qa_appts.json) + EMID=$(jq -r ".content[$i].employeeId // empty" /tmp/qa_appts.json) + [ -n "$SVID" ] && check "appt[$i] service $SVID" 200 GET "/services/$SVID" + [ -n "$STID" ] && check "appt[$i] store $STID" 200 GET "/stores/$STID" + [ -n "$CID" ] && check "appt[$i] customer $CID" 200 GET "/users/$CID" "${A[@]}" + [ -n "$EMID" ] && check "appt[$i] employee $EMID" 200 GET "/users/$EMID" "${A[@]}" +done + +echo "--- FK integrity: inventory ---" +check "inv sample" 200 GET "/inventory?size=3" "${A[@]}" +cp /tmp/qa_body.json /tmp/qa_inv.json +for i in 0 1 2; do + ISTID=$(jq -r ".content[$i].storeId // empty" /tmp/qa_inv.json) + IPID=$(jq -r ".content[$i].prodId // empty" /tmp/qa_inv.json) + [ -n "$ISTID" ] && check "inv[$i] store $ISTID" 200 GET "/stores/$ISTID" + [ -n "$IPID" ] && check "inv[$i] product $IPID" 200 GET "/products/$IPID" +done + +echo "--- FK integrity: products ---" +check "products sample" 200 GET "/products?size=3" +cp /tmp/qa_body.json /tmp/qa_prods.json +for i in 0 1 2; do + CATID=$(jq -r ".content[$i].categoryId // empty" /tmp/qa_prods.json) + [ -n "$CATID" ] && check "prod[$i] category $CATID" 200 GET "/categories/$CATID" +done + +echo "--- Cross-ref violations ---" +check "sale bad storeId" 404 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":999,"paymentMethod":"Cash","channel":"IN_STORE","items":[{"prodId":1,"quantity":1}]}' +check "appt bad serviceId" 404 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"serviceId":999,"petId":53,"customerId":32,"storeId":2,"employeeId":7,"appointmentDate":"2027-09-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}' +check "appt bad customerId" 404 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"serviceId":1,"petId":53,"customerId":999,"storeId":2,"employeeId":7,"appointmentDate":"2027-09-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}' +check "appt bad employeeId" 404 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"serviceId":1,"petId":53,"customerId":32,"storeId":2,"employeeId":999,"appointmentDate":"2027-09-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}' + +echo "--- Dangling reference (FK on delete) ---" +DEL_CAT_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE/categories/1" "${A[@]}" 2>/dev/null) +if [ "$DEL_CAT_CODE" != "204" ] && [ "$DEL_CAT_CODE" != "200" ]; then PASS=$((PASS+1)) +else FAIL=$((FAIL+1)); echo "FAIL: DELETE /categories/1 should fail (FK) — got $DEL_CAT_CODE"; fi + +DEL_STORE_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE/stores/1" "${A[@]}" 2>/dev/null) +if [ "$DEL_STORE_CODE" != "204" ] && [ "$DEL_STORE_CODE" != "200" ]; then PASS=$((PASS+1)) +else FAIL=$((FAIL+1)); echo "FAIL: DELETE /stores/1 should fail (FK) — got $DEL_STORE_CODE"; fi + +echo "--- Avatar file references ---" +check "admin avatar file" 200 GET "/users/1/avatar/file" "${A[@]}" +check "user 16 avatar file" 200 GET "/users/16/avatar/file" "${A[@]}" +check "product 1 image" 200 GET "/products/1/image" + +############################################################################### +echo "" +echo "========== E — EXISTENCE ==========" +############################################################################### + +echo "--- Required fields never null ---" +check "products fields" 200 GET "/products?size=100" +check_true "products non-null fields" '.content | all(.prodId != null and .prodName != null and .prodPrice != null and .categoryId != null)' + +check "users fields" 200 GET "/users?size=100" "${A[@]}" +check_true "users non-null fields" '.content | all(.id != null and .firstName != null and .lastName != null and .role != null)' + +check "sales fields" 200 GET "/sales?size=50" "${A[@]}" +check_true "sales non-null fields" '.content | all(.saleId != null and .saleDate != null and .totalAmount != null and .storeId != null and .employeeId != null)' + +check "appts fields" 200 GET "/appointments?size=50" "${A[@]}" +check_true "appts non-null fields" '.content | all(.appointmentId != null and .serviceId != null and .customerId != null and .storeId != null and .appointmentDate != null and .appointmentTime != null and .appointmentStatus != null)' + +check "coupons fields" 200 GET "/coupons" "${A[@]}" +check_true "coupons non-null fields" '[.[] | .couponId != null and .couponCode != null and .discountType != null and .discountValue != null] | all' + +echo "--- Avatar existence for all roles ---" +check "admin me avatar" 200 GET "/auth/me/avatar/file" "${A[@]}" +check "staff me avatar" 200 GET "/auth/me/avatar/file" "${S[@]}" +check "customer me avatar" 200 GET "/auth/me/avatar/file" "${C[@]}" + +echo "--- Empty search returns valid structure ---" +check "empty product search" 200 GET "/products?q=zzzznothing" +check_true "empty search .content is array" '.content | type == "array"' +check_true "empty search totalElements 0" '.totalElements == 0' + +check "empty pet species" 200 GET "/pets?species=Unicorn" +check_true "unicorn totalElements 0" '.totalElements == 0' + +check "future appts" 200 GET "/appointments?date=2099-12-31" "${A[@]}" +check_true "far future totalElements 0" '.totalElements == 0' + +check "nonexistent activity log" 200 GET "/activity-logs?search=xyznonexistent" "${A[@]}" +check_true "activity log empty array" 'length == 0' + +echo "--- Missing parameter handling ---" +check_not_500 "cart no storeId" GET "/cart" "${C[@]}" +check_not_500 "availability no params" GET "/appointments/availability" +check_not_500 "checkout empty body" POST "/cart/checkout" "${C[@]}" "${J[@]}" -d '{}' + +############################################################################### +echo "" +echo "========== C — CARDINALITY ==========" +############################################################################### + +echo "--- Unique constraints ---" +check "dup username admin" 409 POST "/auth/register" "${J[@]}" -d '{"username":"admin","password":"pass1234","firstName":"Q","lastName":"A","email":"qadup1@test.com"}' +DUP_CODE=$(cat /tmp/qa_body.json | jq -r '.status // empty' 2>/dev/null) +if [ "$?" != "0" ] || [ "$(curl -s -o /dev/null -w "%{http_code}" /dev/null 2>/dev/null)" = "" ]; then true; fi + +check "dup email admin" 409 POST "/auth/register" "${J[@]}" -d '{"username":"qauniq99","password":"pass1234","firstName":"Q","lastName":"A","email":"admin@petshop.com"}' + +check "dup coupon code" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"WELCOME10","discountType":"PERCENTAGE","discountValue":10}' + +echo "--- One-to-many counts ---" +check "sale 1 items" 200 GET "/sales/1" "${A[@]}" +check_true "sale has >= 1 item" '(.items | length) >= 1' + +check "service 1 species" 200 GET "/services/1" +check_true "service has >= 1 species" '(.species | length) >= 1' + +check "products page size 5" 200 GET "/products?size=5" +check_true "content length <= 5" '(.content | length) <= 5' +check_true "content length = min(5, total)" '(.content | length) == (if .totalElements < 5 then .totalElements else 5 end)' + +echo "--- Pagination math ---" +check "pagination check" 200 GET "/products?page=0&size=10" +check_field "page number is 0" '.number' '0' +check_field "page size is 10" '.size' '10' + +check "products default page" 200 GET "/products" +TOTAL=$(jq -r '.totalElements' /tmp/qa_body.json) +SIZE=$(jq -r '.size' /tmp/qa_body.json) +PAGES=$(jq -r '.totalPages' /tmp/qa_body.json) +EXPECTED_PAGES=$(( (TOTAL + SIZE - 1) / SIZE )) +if [ "$PAGES" = "$EXPECTED_PAGES" ]; then PASS=$((PASS+1)) +else FAIL=$((FAIL+1)); echo "FAIL: totalPages=$PAGES expected=$EXPECTED_PAGES (total=$TOTAL size=$SIZE)"; fi + +check "activity logs limit 3" 200 GET "/activity-logs?limit=3" "${A[@]}" +check_true "logs length <= 3" 'length <= 3' + +echo "--- Cart cardinality ---" +curl -s -o /dev/null -X DELETE "$BASE/cart" "${C[@]}" 2>/dev/null +curl -s -o /dev/null -X POST "$BASE/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":1}' 2>/dev/null +check "cart after add 1" 200 GET "/cart?storeId=1" "${C[@]}" +CART_LEN1=$(jq '.items | length' /tmp/qa_body.json 2>/dev/null) + +curl -s -o /dev/null -X POST "$BASE/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":2,"storeId":1,"quantity":1}' 2>/dev/null +check "cart after add 2" 200 GET "/cart?storeId=1" "${C[@]}" +CART_LEN2=$(jq '.items | length' /tmp/qa_body.json 2>/dev/null) + +curl -s -o /dev/null -X POST "$BASE/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":1}' 2>/dev/null +check "cart after re-add 1" 200 GET "/cart?storeId=1" "${C[@]}" +CART_LEN3=$(jq '.items | length' /tmp/qa_body.json 2>/dev/null) + +if [ "$CART_LEN2" = "2" ]; then PASS=$((PASS+1)) +else FAIL=$((FAIL+1)); echo "FAIL: cart should have 2 items after adding 2 products, got $CART_LEN2"; fi +if [ "$CART_LEN3" = "2" ]; then PASS=$((PASS+1)) +else FAIL=$((FAIL+1)); echo "FAIL: cart should still have 2 items (qty merged), got $CART_LEN3"; fi + +curl -s -o /dev/null -X DELETE "$BASE/cart" "${C[@]}" 2>/dev/null + +echo "--- Availability cardinality ---" +check "avail no dups" 200 GET "/appointments/availability?storeId=1&serviceId=1&date=2027-09-01" "${A[@]}" +check_true "no duplicate times" '[. | group_by(.) | all(length == 1)]' + +############################################################################### +echo "" +echo "========== T — TIME ==========" +############################################################################### + +echo "--- Timestamp freshness ---" +NOW_EPOCH=$(date -u +%s) +check "create product" 201 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_TIME_TEST","prodPrice":5.55,"categoryId":1}' +TPROD_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json) +[ -n "$TPROD_ID" ] && CLEANUP_PRODUCT_IDS+=("$TPROD_ID") + +if [ -n "$TPROD_ID" ]; then + check "get created product" 200 GET "/products/$TPROD_ID" + CREATED_AT=$(jq -r '.createdAt // empty' /tmp/qa_body.json) + if [ -n "$CREATED_AT" ]; then + CREATED_EPOCH=$(date -u -d "$CREATED_AT" +%s 2>/dev/null || echo "0") + DIFF=$((NOW_EPOCH - CREATED_EPOCH)) + if [ "$DIFF" -ge "-10" ] && [ "$DIFF" -le "120" ]; then PASS=$((PASS+1)) + else FAIL=$((FAIL+1)); echo "FAIL: createdAt not recent (diff=${DIFF}s)"; fi + else + FAIL=$((FAIL+1)); echo "FAIL: createdAt is null" + fi + + check "update product" 200 PUT "/products/$TPROD_ID" "${A[@]}" "${J[@]}" -d "{\"prodName\":\"QA_TIME_UPD\",\"prodPrice\":6.66,\"categoryId\":1}" + check "get updated product" 200 GET "/products/$TPROD_ID" + check_true "updatedAt >= createdAt" '(.updatedAt // .createdAt) >= .createdAt' +fi + +check "create sale" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"paymentMethod":"Cash","channel":"IN_STORE","items":[{"prodId":2,"quantity":1}]}' +TSALE_ID=$(jq -r '.saleId // empty' /tmp/qa_body.json) +[ -n "$TSALE_ID" ] && CLEANUP_SALE_IDS+=("$TSALE_ID") +if [ -n "$TSALE_ID" ]; then + check "get sale" 200 GET "/sales/$TSALE_ID" "${A[@]}" + check_true "saleDate has T (recent)" '.saleDate | test("T")' +fi + +check "conversations for chat" 200 GET "/chat/conversations" "${C[@]}" +CHAT_CONV=$(jq -r '.[0].conversationId // empty' /tmp/qa_body.json) +if [ -n "$CHAT_CONV" ]; then + curl -s -o /dev/null -X POST "$BASE/chat/conversations/$CHAT_CONV/messages" "${C[@]}" "${J[@]}" -d '{"content":"QA time test message"}' 2>/dev/null + check "get chat messages" 200 GET "/chat/conversations/$CHAT_CONV/messages" "${C[@]}" + LAST_TS=$(jq -r '.[-1].timestamp // empty' /tmp/qa_body.json) + if [ -n "$LAST_TS" ]; then + MSG_EPOCH=$(date -u -d "$LAST_TS" +%s 2>/dev/null || echo "0") + MDIFF=$((NOW_EPOCH - MSG_EPOCH)) + if [ "$MDIFF" -ge "-10" ] && [ "$MDIFF" -le "300" ]; then PASS=$((PASS+1)) + else FAIL=$((FAIL+1)); echo "FAIL: chat message timestamp not recent (diff=${MDIFF}s)"; fi + fi +fi + +echo "--- Temporal constraints ---" +TODAY=$(date -u +%Y-%m-%d) +check "scheduled appts" 200 GET "/appointments?status=Scheduled&size=50" "${A[@]}" +check_true "scheduled dates >= today" "[.content[].appointmentDate] | all(. >= \"$TODAY\")" + +check "coupons temporal" 200 GET "/coupons" "${A[@]}" +NOW_ISO=$(date -u +%Y-%m-%dT%H:%M:%S) +check_true "active coupons endsAt valid" "[.[] | select(.endsAt != null) | .endsAt] | all(. >= \"$NOW_ISO\" or . == null)" + +echo "--- Sequencing ---" +if [ -n "${CHAT_CONV:-}" ]; then + check "chat sequence" 200 GET "/chat/conversations/$CHAT_CONV/messages" "${C[@]}" + check_true "chat timestamps non-decreasing" '. as $a | [range(1;length)] | all(. as $i | $a[$i].timestamp >= $a[$i-1].timestamp)' +fi + +check "activity log sequence" 200 GET "/activity-logs?limit=20" "${A[@]}" +check_true "logs non-increasing" '. as $a | [range(1;length)] | all(. as $i | $a[$i].logTimestamp <= $a[$i-1].logTimestamp)' + +echo "--- Token timing ---" +FRESH_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' | jq -r '.token') +check "fresh token works" 200 GET "/auth/me" -H "Authorization: Bearer $FRESH_TOKEN" + +curl -s -o /dev/null -X POST "$BASE/auth/logout" -H "Authorization: Bearer $FRESH_TOKEN" 2>/dev/null +LOGOUT_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X GET "$BASE/auth/me" -H "Authorization: Bearer $FRESH_TOKEN" 2>/dev/null) +if [ "$LOGOUT_CODE" = "401" ]; then PASS=$((PASS+1)) +else FAIL=$((FAIL+1)); echo "FAIL: old token after logout — expected 401 got $LOGOUT_CODE"; fi + +ADMIN_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' | jq -r '.token') +A=(-H "Authorization: Bearer $ADMIN_TOKEN") + +echo "" +echo "=========================================" +echo "CORRECT RESULTS: $PASS passed, $FAIL failed" +echo "=========================================" -- 2.49.1 From b6378c30490bc83ecba16f409112620366979c0e Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 08:02:45 -0600 Subject: [PATCH 26/34] handle missing exception types --- .../exception/GlobalExceptionHandler.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java index 3801b3ea..092263d9 100644 --- a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java @@ -11,10 +11,13 @@ import org.springframework.security.authentication.DisabledException; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; +import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.resource.NoResourceFoundException; @@ -117,11 +120,31 @@ public class GlobalExceptionHandler { return buildErrorResponse(HttpStatus.BAD_REQUEST, "Invalid sort field: " + ex.getPropertyName(), ex, request); } + @ExceptionHandler(org.hibernate.query.PathException.class) + public ResponseEntity handleHibernatePathException(org.hibernate.query.PathException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Invalid query field reference", ex, request); + } + @ExceptionHandler(PetService.ForbiddenImageAccessException.class) public ResponseEntity handleForbiddenImageAccess(PetService.ForbiddenImageAccessException ex, HttpServletRequest request) { return buildErrorResponse(HttpStatus.FORBIDDEN, "Access to this pet image is not allowed", ex, request); } + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Invalid or missing request body", ex, request); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity handleMissingParam(MissingServletRequestParameterException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Missing required parameter: " + ex.getParameterName(), ex, request); + } + + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public ResponseEntity handleMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Unsupported content type", ex, request); + } + @ExceptionHandler(IOException.class) public ResponseEntity handleIOException(IOException ex, HttpServletRequest request) { return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), ex, request); -- 2.49.1 From bebdf7094ec1dc43ff361ca8de0301ccc818d79d Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 08:15:59 -0600 Subject: [PATCH 27/34] catch sort query exceptions --- .../backend/exception/GlobalExceptionHandler.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java index 092263d9..ed070aed 100644 --- a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java @@ -120,8 +120,8 @@ public class GlobalExceptionHandler { return buildErrorResponse(HttpStatus.BAD_REQUEST, "Invalid sort field: " + ex.getPropertyName(), ex, request); } - @ExceptionHandler(org.hibernate.query.PathException.class) - public ResponseEntity handleHibernatePathException(org.hibernate.query.PathException ex, HttpServletRequest request) { + @ExceptionHandler({org.hibernate.query.PathException.class, org.hibernate.query.sqm.PathElementException.class}) + public ResponseEntity handleHibernatePathException(Exception ex, HttpServletRequest request) { return buildErrorResponse(HttpStatus.BAD_REQUEST, "Invalid query field reference", ex, request); } @@ -150,6 +150,11 @@ public class GlobalExceptionHandler { return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), ex, request); } + @ExceptionHandler(org.springframework.dao.InvalidDataAccessApiUsageException.class) + public ResponseEntity handleInvalidDataAccess(org.springframework.dao.InvalidDataAccessApiUsageException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Invalid query or sort parameter", ex, request); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleGenericException(Exception ex, HttpServletRequest request) { String message = ex.getMessage() == null || ex.getMessage().isBlank() -- 2.49.1 From 72a3e8d128303499f80a705f037c692c6ec0cb05 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 08:24:28 -0600 Subject: [PATCH 28/34] auto-complete scheduled appointments --- .../petshop/backend/repository/AppointmentRepository.java | 2 +- .../com/petshop/backend/service/AppointmentService.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index b23b1902..774c1427 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -48,7 +48,7 @@ public interface AppointmentRepository extends JpaRepository @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.id IN :employeeIds AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByEmployeeIdInAndAppointmentDate(@Param("employeeIds") List employeeIds, @Param("date") LocalDate date); - @Query("SELECT a FROM Appointment a WHERE (a.appointmentDate < :currentDate OR (a.appointmentDate = :currentDate AND a.appointmentTime < :currentTime)) AND LOWER(a.appointmentStatus) = 'booked'") + @Query("SELECT a FROM Appointment a WHERE (a.appointmentDate < :currentDate OR (a.appointmentDate = :currentDate AND a.appointmentTime < :currentTime)) AND LOWER(a.appointmentStatus) IN ('booked', 'scheduled')") List findPastBookedAppointments(@Param("currentDate") LocalDate currentDate, @Param("currentTime") LocalTime currentTime); List findByPet_Id(Long petId); diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 832dc12e..048bdc37 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -279,8 +279,12 @@ public class AppointmentService { LocalDate tomorrow = currentDate.plusDays(1); - List tomorrowAppointments = appointmentRepository + List tomorrowBooked = appointmentRepository .findByAppointmentDateAndAppointmentStatusIgnoreCase(tomorrow, "Booked"); + List tomorrowScheduled = appointmentRepository + .findByAppointmentDateAndAppointmentStatusIgnoreCase(tomorrow, "Scheduled"); + List tomorrowAppointments = new java.util.ArrayList<>(tomorrowBooked); + tomorrowAppointments.addAll(tomorrowScheduled); for (Appointment appointment : tomorrowAppointments) { eventPublisher.publishEvent(new AppointmentReminderEvent(appointment.getAppointmentId())); } -- 2.49.1 From 6171b0f2f58ab714094f0f9227a14a3ae3fd9c2c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 09:42:21 -0600 Subject: [PATCH 29/34] title-case all DB strings --- .../backend/controller/AuthController.java | 4 +- .../dto/adoption/CustomerAdoptionRequest.java | 2 +- .../dto/auth/ProfileUpdateRequest.java | 3 + .../backend/dto/chat/MessageRequest.java | 1 + .../backend/dto/common/CouponRequest.java | 1 + .../petshop/backend/dto/sale/SaleRequest.java | 5 + .../java/com/petshop/backend/entity/Sale.java | 2 +- .../backend/service/CouponService.java | 2 +- .../petshop/backend/service/SaleService.java | 2 +- .../db/migration/V1__target_baseline.sql | 2 +- .../resources/db/migration/V2__seed_data.sql | 578 +++++++++--------- 11 files changed, 307 insertions(+), 295 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index eb91c6dc..032518ec 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -344,9 +344,11 @@ public class AuthController { @PostMapping("/logout") public ResponseEntity logout() { + User user = authHelper.getAuthenticatedUser(); + user.setTokenVersion(user.getTokenVersion() + 1); + userRepository.save(user); Map response = new HashMap<>(); response.put("message", "Logged out successfully"); - response.put("note", "Token remains valid until expiration. Clear token from client storage."); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java b/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java index 287c1d6d..4272b2db 100644 --- a/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/CustomerAdoptionRequest.java @@ -12,7 +12,7 @@ public class CustomerAdoptionRequest { private Long sourceStoreId; - @NotNull(message = "Appointment date is required") + @NotNull(message = "Adoption date is required") private LocalDate adoptionDate; public Long getPetId() { diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java b/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java index dc1c98c0..97b978b0 100644 --- a/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java @@ -1,14 +1,17 @@ package com.petshop.backend.dto.auth; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import java.util.Objects; public class ProfileUpdateRequest { @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") + @Pattern(regexp = "^(?!\\s*$).+", message = "Username must not be blank") private String username; @Email(message = "Email must be valid") + @Pattern(regexp = "^(?!\\s*$).+", message = "Email must not be blank") private String email; @Size(max = 50, message = "First name must not exceed 50 characters") diff --git a/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java b/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java index cecedfbd..37dcd683 100644 --- a/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java @@ -1,6 +1,7 @@ package com.petshop.backend.dto.chat; public class MessageRequest { + @jakarta.validation.constraints.Size(max = 10000, message = "Message content must not exceed 10000 characters") private String content; private String attachmentUrl; private String attachmentName; diff --git a/backend/src/main/java/com/petshop/backend/dto/common/CouponRequest.java b/backend/src/main/java/com/petshop/backend/dto/common/CouponRequest.java index 83531a74..ffa7f766 100644 --- a/backend/src/main/java/com/petshop/backend/dto/common/CouponRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/common/CouponRequest.java @@ -11,6 +11,7 @@ public class CouponRequest { private String couponCode; @NotBlank(message = "Discount type is required") + @jakarta.validation.constraints.Pattern(regexp = "^(Percent|Percentage|Fixed|Flat)$", flags = jakarta.validation.constraints.Pattern.Flag.CASE_INSENSITIVE, message = "Discount type must be Percent, Percentage, Fixed, or Flat") private String discountType; @NotNull(message = "Discount value is required") diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java index db9eb6cd..b0e7989f 100644 --- a/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java @@ -1,8 +1,10 @@ package com.petshop.backend.dto.sale; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import java.util.List; import java.util.Objects; @@ -10,6 +12,8 @@ public class SaleRequest { @NotNull(message = "Store ID is required") private Long storeId; + @NotBlank(message = "Payment method is required") + @Pattern(regexp = "^(Cash|Card)$", message = "Payment method must be Cash or Card") private String paymentMethod; @NotEmpty(message = "At least one item is required") @@ -22,6 +26,7 @@ public class SaleRequest { private Long customerId; + @Pattern(regexp = "^(In Store|Online)$", message = "Channel must be In Store or Online") private String channel; private Long couponId; diff --git a/backend/src/main/java/com/petshop/backend/entity/Sale.java b/backend/src/main/java/com/petshop/backend/entity/Sale.java index b052e17d..d1134b58 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Sale.java +++ b/backend/src/main/java/com/petshop/backend/entity/Sale.java @@ -47,7 +47,7 @@ public class Sale { private Sale originalSale; @Column(nullable = false, length = 20) - private String channel = "IN_STORE"; + private String channel = "In Store"; @ManyToOne @JoinColumn(name = "cartId") diff --git a/backend/src/main/java/com/petshop/backend/service/CouponService.java b/backend/src/main/java/com/petshop/backend/service/CouponService.java index 18ff269c..cf56188f 100644 --- a/backend/src/main/java/com/petshop/backend/service/CouponService.java +++ b/backend/src/main/java/com/petshop/backend/service/CouponService.java @@ -89,7 +89,7 @@ public class CouponService { } private void updateCouponFields(Coupon coupon, CouponRequest request) { - if ("PERCENTAGE".equalsIgnoreCase(request.getDiscountType()) + if (isPercentageType(request.getDiscountType()) && request.getDiscountValue().compareTo(new BigDecimal("100")) >= 0) { throw new BusinessException("Percentage discount must be less than 100"); } diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index c35c07da..d33d11b3 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -84,7 +84,7 @@ public class SaleService { sale.setStore(store); sale.setPaymentMethod(normalizePaymentMethod(request.getPaymentMethod())); sale.setIsRefund(request.getIsRefund() != null ? request.getIsRefund() : false); - sale.setChannel(request.getChannel() != null ? request.getChannel() : "IN_STORE"); + sale.setChannel(request.getChannel() != null ? request.getChannel() : "In Store"); if (request.getCouponId() != null) { Coupon coupon = couponRepository.findByIdForUpdate(request.getCouponId()) diff --git a/backend/src/main/resources/db/migration/V1__target_baseline.sql b/backend/src/main/resources/db/migration/V1__target_baseline.sql index 01a0c34e..bb5d92f3 100644 --- a/backend/src/main/resources/db/migration/V1__target_baseline.sql +++ b/backend/src/main/resources/db/migration/V1__target_baseline.sql @@ -244,7 +244,7 @@ CREATE TABLE IF NOT EXISTS sale ( customerId BIGINT NULL, isRefund BOOLEAN NOT NULL DEFAULT FALSE, originalSaleId BIGINT NULL, - channel VARCHAR(20) NOT NULL DEFAULT 'IN_STORE', + channel VARCHAR(20) NOT NULL DEFAULT 'In Store', cartId BIGINT NULL, couponId BIGINT NULL, subtotalAmount DECIMAL(10, 2) NULL, diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql index 4109ec6a..0a307195 100644 --- a/backend/src/main/resources/db/migration/V2__seed_data.sql +++ b/backend/src/main/resources/db/migration/V2__seed_data.sql @@ -59,19 +59,19 @@ INSERT INTO storeLocation (storeId, storeName, address, phone, email, imageUrl) INSERT INTO users (id, username, password, email, firstName, lastName, fullName, phone, avatarUrl, role, staffRole, primaryStoreId, loyaltyPoints, active, tokenVersion) VALUES (1, 'admin', '$2y$10$ok/BmOn/pyyamTeNmUDiB.OfLCduQlZSAaRLlupM/cZb7ZhiBriVe', 'admin@petshop.com', 'Admin', 'User', 'Admin User', '000-000-1000', 'https://images.petshop.local/users/001.webp', 'ADMIN', 'ADMINISTRATOR', 1, 0, 1, 0), -(2, 'morgan.lee', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'morgan.lee@petshop.com', 'Morgan', 'Lee', 'Morgan Lee', '403-700-0002', 'https://images.petshop.local/users/002.webp', 'ADMIN', 'OPERATIONS_ADMIN', 2, 0, 1, 0), -(3, 'staff', '$2y$10$23mqbLolo609T/.PC4KfiuY.9HqYEgA8LrJ/fccZ7CmK0/OIsPrfq', 'staff@petshop.com', 'Staff', 'User', 'Staff User', '000-000-1001', 'https://images.petshop.local/users/003.webp', 'STAFF', 'STORE_MANAGER', 1, 0, 1, 0), -(4, 'sara.smith', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'sara.smith@petshop.com', 'Sara', 'Smith', 'Sara Smith', '403-710-0004', 'https://images.petshop.local/users/004.webp', 'STAFF', 'SALES_ASSOCIATE', 1, 0, 1, 0), -(5, 'david.brown', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'david.brown@petshop.com', 'David', 'Brown', 'David Brown', '403-710-0005', 'https://images.petshop.local/users/005.webp', 'STAFF', 'VETERINARY_TECH', 1, 0, 1, 0), -(6, 'priya.patel', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'priya.patel@petshop.com', 'Priya', 'Patel', 'Priya Patel', '403-710-0006', 'https://images.petshop.local/users/006.webp', 'STAFF', 'GROOMER', 1, 0, 1, 0), -(7, 'michael.johnson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'michael.johnson@petshop.com', 'Michael', 'Johnson', 'Michael Johnson', '403-710-0007', 'https://images.petshop.local/users/007.webp', 'STAFF', 'STORE_MANAGER', 2, 0, 1, 0), -(8, 'emma.davis', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'emma.davis@petshop.com', 'Emma', 'Davis', 'Emma Davis', '403-710-0008', 'https://images.petshop.local/users/008.webp', 'STAFF', 'SALES_ASSOCIATE', 2, 0, 1, 0), -(9, 'lucas.turner', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'lucas.turner@petshop.com', 'Lucas', 'Turner', 'Lucas Turner', '403-710-0009', 'https://images.petshop.local/users/009.webp', 'STAFF', 'VETERINARY_TECH', 2, 0, 1, 0), -(10, 'nina.green', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'nina.green@petshop.com', 'Nina', 'Green', 'Nina Green', '403-710-0010', 'https://images.petshop.local/users/010.webp', 'STAFF', 'GROOMER', 2, 0, 1, 0), -(11, 'lisa.williams', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'lisa.williams@petshop.com', 'Lisa', 'Williams', 'Lisa Williams', '403-710-0011', 'https://images.petshop.local/users/011.webp', 'STAFF', 'STORE_MANAGER', 3, 0, 1, 0), -(12, 'daniel.moore', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'daniel.moore@petshop.com', 'Daniel', 'Moore', 'Daniel Moore', '403-710-0012', 'https://images.petshop.local/users/012.webp', 'STAFF', 'SALES_ASSOCIATE', 3, 0, 1, 0), -(13, 'chloe.martin', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'chloe.martin@petshop.com', 'Chloe', 'Martin', 'Chloe Martin', '403-710-0013', 'https://images.petshop.local/users/013.webp', 'STAFF', 'VETERINARY_TECH', 3, 0, 1, 0), -(14, 'owen.baker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'owen.baker@petshop.com', 'Owen', 'Baker', 'Owen Baker', '403-710-0014', 'https://images.petshop.local/users/014.webp', 'STAFF', 'GROOMER', 3, 0, 1, 0), +(2, 'morgan.lee', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'morgan.lee@petshop.com', 'Morgan', 'Lee', 'Morgan Lee', '403-700-0002', 'https://images.petshop.local/users/002.webp', 'ADMIN', 'Operations Admin', 2, 0, 1, 0), +(3, 'staff', '$2y$10$23mqbLolo609T/.PC4KfiuY.9HqYEgA8LrJ/fccZ7CmK0/OIsPrfq', 'staff@petshop.com', 'Staff', 'User', 'Staff User', '000-000-1001', 'https://images.petshop.local/users/003.webp', 'STAFF', 'Store Manager', 1, 0, 1, 0), +(4, 'sara.smith', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'sara.smith@petshop.com', 'Sara', 'Smith', 'Sara Smith', '403-710-0004', 'https://images.petshop.local/users/004.webp', 'STAFF', 'Sales Associate', 1, 0, 1, 0), +(5, 'david.brown', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'david.brown@petshop.com', 'David', 'Brown', 'David Brown', '403-710-0005', 'https://images.petshop.local/users/005.webp', 'STAFF', 'Veterinary Tech', 1, 0, 1, 0), +(6, 'priya.patel', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'priya.patel@petshop.com', 'Priya', 'Patel', 'Priya Patel', '403-710-0006', 'https://images.petshop.local/users/006.webp', 'STAFF', 'Groomer', 1, 0, 1, 0), +(7, 'michael.johnson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'michael.johnson@petshop.com', 'Michael', 'Johnson', 'Michael Johnson', '403-710-0007', 'https://images.petshop.local/users/007.webp', 'STAFF', 'Store Manager', 2, 0, 1, 0), +(8, 'emma.davis', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'emma.davis@petshop.com', 'Emma', 'Davis', 'Emma Davis', '403-710-0008', 'https://images.petshop.local/users/008.webp', 'STAFF', 'Sales Associate', 2, 0, 1, 0), +(9, 'lucas.turner', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'lucas.turner@petshop.com', 'Lucas', 'Turner', 'Lucas Turner', '403-710-0009', 'https://images.petshop.local/users/009.webp', 'STAFF', 'Veterinary Tech', 2, 0, 1, 0), +(10, 'nina.green', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'nina.green@petshop.com', 'Nina', 'Green', 'Nina Green', '403-710-0010', 'https://images.petshop.local/users/010.webp', 'STAFF', 'Groomer', 2, 0, 1, 0), +(11, 'lisa.williams', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'lisa.williams@petshop.com', 'Lisa', 'Williams', 'Lisa Williams', '403-710-0011', 'https://images.petshop.local/users/011.webp', 'STAFF', 'Store Manager', 3, 0, 1, 0), +(12, 'daniel.moore', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'daniel.moore@petshop.com', 'Daniel', 'Moore', 'Daniel Moore', '403-710-0012', 'https://images.petshop.local/users/012.webp', 'STAFF', 'Sales Associate', 3, 0, 1, 0), +(13, 'chloe.martin', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'chloe.martin@petshop.com', 'Chloe', 'Martin', 'Chloe Martin', '403-710-0013', 'https://images.petshop.local/users/013.webp', 'STAFF', 'Veterinary Tech', 3, 0, 1, 0), +(14, 'owen.baker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'owen.baker@petshop.com', 'Owen', 'Baker', 'Owen Baker', '403-710-0014', 'https://images.petshop.local/users/014.webp', 'STAFF', 'Groomer', 3, 0, 1, 0), (15, 'customer', '$2y$10$fgIlTHDYUOzvbczwdhQP7..YuAHr2cGODb9OBQJqole3AkiY4CGUq', 'customer@petshop.com', 'Test', 'Customer', 'Test Customer', '000-000-1002', 'https://images.petshop.local/users/015.webp', 'CUSTOMER', 'CUSTOMER', NULL, 0, 1, 0), (16, 'alex.brown', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.brown@gmail.com', 'Alex', 'Brown', 'Alex Brown', '403-730-0016', 'https://images.petshop.local/users/016.webp', 'CUSTOMER', 'CUSTOMER', NULL, 12, 1, 0), (17, 'alex.clark', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.clark@gmail.com', 'Alex', 'Clark', 'Alex Clark', '403-730-0017', 'https://images.petshop.local/users/017.webp', 'CUSTOMER', 'CUSTOMER', NULL, 15, 1, 0), @@ -830,14 +830,14 @@ INSERT INTO purchaseOrder (purchaseOrderId, supId, storeId, orderDate) VALUES (36, 10, 3, '2026-04-22'); INSERT INTO coupon (couponId, couponCode, discountType, discountValue, minOrderAmount, active, startsAt, endsAt, usageLimit) VALUES -(1, 'NOCODE', 'FIXED', 0.00, 0.00, 1, NULL, NULL, NULL), -(2, 'WELCOME10', 'PERCENT', 10.00, 50.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 300), -(3, 'TREAT5', 'FIXED', 5.00, 25.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 500), -(4, 'GROOM15', 'PERCENT', 15.00, 60.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 200), -(5, 'FISHCARE8', 'FIXED', 8.00, 40.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 150), -(6, 'BIRD10', 'PERCENT', 10.00, 30.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 150), -(7, 'SPRING12', 'PERCENT', 12.00, 75.00, 1, '2026-03-01 00:00:00', '2026-05-31 23:59:59', 180), -(8, 'NEWPET20', 'FIXED', 20.00, 100.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 120); +(1, 'NOCODE', 'Fixed', 0.00, 0.00, 1, NULL, NULL, NULL), +(2, 'WELCOME10', 'Percent', 10.00, 50.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 300), +(3, 'TREAT5', 'Fixed', 5.00, 25.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 500), +(4, 'GROOM15', 'Percent', 15.00, 60.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 200), +(5, 'FISHCARE8', 'Fixed', 8.00, 40.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 150), +(6, 'BIRD10', 'Percent', 10.00, 30.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 150), +(7, 'SPRING12', 'Percent', 12.00, 75.00, 1, '2026-03-01 00:00:00', '2026-05-31 23:59:59', 180), +(8, 'NEWPET20', 'Fixed', 20.00, 100.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 120); INSERT INTO pet (petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, ownerUserId, storeId) VALUES (1, 'Buddy', 'Dog', 'Corgi', 2, 'Available', 466.80, 'https://images.petshop.local/pets/001.webp', NULL, 1), @@ -942,96 +942,96 @@ INSERT INTO pet (petId, petName, petSpecies, petBreed, petAge, petStatus, petPri (100, 'Comet', 'Bird', 'Lovebird', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/100.webp', 79, NULL); INSERT INTO appointment (appointmentId, serviceId, petId, customerId, storeId, employeeId, appointmentDate, appointmentTime, appointmentStatus) VALUES -(1, 2, 37, 16, 1, 3, '2026-01-07', '09:00:00', 'COMPLETED'), -(2, 8, 38, 17, 2, 8, '2026-01-09', '10:30:00', 'COMPLETED'), -(3, 4, 39, 18, 3, 13, '2026-01-11', '13:00:00', 'MISSED'), -(4, 8, 40, 19, 1, 6, '2026-01-13', '14:30:00', 'CANCELLED'), -(5, 5, 41, 20, 2, 7, '2026-01-15', '16:00:00', 'COMPLETED'), -(6, 8, 42, 21, 3, 12, '2026-01-17', '09:00:00', 'COMPLETED'), -(7, 2, 43, 22, 1, 5, '2026-01-19', '10:30:00', 'COMPLETED'), -(8, 8, 44, 23, 2, 10, '2026-01-21', '13:00:00', 'MISSED'), -(9, 4, 45, 24, 3, 11, '2026-01-23', '14:30:00', 'CANCELLED'), -(10, 8, 46, 25, 1, 4, '2026-01-25', '16:00:00', 'COMPLETED'), -(11, 6, 47, 26, 2, 9, '2026-01-27', '09:00:00', 'COMPLETED'), -(12, 7, 48, 27, 3, 14, '2026-01-29', '10:30:00', 'COMPLETED'), -(13, 2, 49, 28, 1, 3, '2026-01-31', '13:00:00', 'MISSED'), -(14, 4, 50, 29, 2, 8, '2026-02-02', '14:30:00', 'CANCELLED'), -(15, 5, 51, 30, 3, 13, '2026-02-04', '16:00:00', 'COMPLETED'), -(16, 1, 52, 31, 1, 6, '2026-02-06', '09:00:00', 'COMPLETED'), -(17, 2, 53, 32, 2, 7, '2026-02-08', '10:30:00', 'COMPLETED'), -(18, 3, 54, 33, 3, 12, '2026-02-10', '13:00:00', 'MISSED'), -(19, 4, 55, 34, 2, 9, '2026-02-12', '14:30:00', 'CANCELLED'), -(20, 5, 56, 35, 3, 14, '2026-02-14', '16:00:00', 'COMPLETED'), -(21, 1, 57, 36, 1, 3, '2026-02-16', '09:00:00', 'COMPLETED'), -(22, 4, 58, 37, 2, 8, '2026-02-18', '10:30:00', 'COMPLETED'), -(23, 4, 59, 38, 3, 13, '2026-02-20', '13:00:00', 'MISSED'), -(24, 5, 60, 39, 1, 6, '2026-02-22', '14:30:00', 'CANCELLED'), -(25, 2, 61, 40, 2, 7, '2026-02-24', '16:00:00', 'COMPLETED'), -(26, 1, 62, 41, 3, 12, '2026-02-26', '09:00:00', 'COMPLETED'), -(27, 2, 63, 42, 1, 5, '2026-02-28', '10:30:00', 'COMPLETED'), -(28, 3, 64, 43, 2, 10, '2026-03-02', '13:00:00', 'MISSED'), -(29, 2, 65, 44, 3, 11, '2026-03-04', '14:30:00', 'CANCELLED'), -(30, 8, 66, 45, 1, 4, '2026-03-06', '16:00:00', 'COMPLETED'), -(31, 2, 67, 46, 2, 9, '2026-03-08', '09:00:00', 'COMPLETED'), -(32, 5, 68, 47, 3, 14, '2026-03-10', '10:30:00', 'COMPLETED'), -(33, 3, 69, 48, 1, 3, '2026-03-12', '13:00:00', 'MISSED'), -(34, 4, 70, 49, 2, 8, '2026-03-14', '14:30:00', 'CANCELLED'), -(35, 5, 71, 50, 3, 13, '2026-03-16', '16:00:00', 'COMPLETED'), -(36, 7, 72, 51, 1, 6, '2026-03-18', '09:00:00', 'COMPLETED'), -(37, 4, 73, 52, 2, 7, '2026-03-20', '10:30:00', 'COMPLETED'), -(38, 4, 74, 53, 3, 12, '2026-03-22', '13:00:00', 'MISSED'), -(39, 4, 75, 54, 1, 5, '2026-03-24', '14:30:00', 'CANCELLED'), -(40, 5, 76, 55, 2, 10, '2026-03-26', '16:00:00', 'COMPLETED'), -(41, 1, 77, 56, 3, 11, '2026-03-28', '09:00:00', 'COMPLETED'), -(42, 2, 78, 57, 1, 4, '2026-03-30', '10:30:00', 'COMPLETED'), -(43, 6, 79, 58, 2, 9, '2026-04-01', '13:00:00', 'BOOKED'), -(44, 8, 80, 59, 3, 14, '2026-04-03', '14:30:00', 'BOOKED'), -(45, 5, 81, 60, 1, 3, '2026-04-05', '16:00:00', 'BOOKED'), -(46, 3, 82, 61, 2, 8, '2026-04-07', '09:00:00', 'BOOKED'), -(47, 2, 83, 62, 3, 13, '2026-04-09', '10:30:00', 'BOOKED'), -(48, 3, 84, 63, 1, 6, '2026-04-11', '13:00:00', 'BOOKED'), -(49, 4, 85, 64, 2, 7, '2026-04-13', '14:30:00', 'BOOKED'), -(50, 4, 86, 65, 3, 12, '2026-04-15', '16:00:00', 'BOOKED'), -(51, 4, 87, 66, 1, 5, '2026-04-17', '09:00:00', 'BOOKED'), -(52, 2, 88, 67, 2, 10, '2026-04-19', '10:30:00', 'BOOKED'), -(53, 2, 89, 68, 3, 11, '2026-04-21', '13:00:00', 'BOOKED'), -(54, 4, 90, 69, 1, 4, '2026-04-23', '14:30:00', 'BOOKED'), -(55, 5, 91, 70, 2, 9, '2026-04-25', '16:00:00', 'BOOKED'), -(56, 1, 92, 71, 3, 14, '2026-04-27', '09:00:00', 'BOOKED'), -(57, 2, 93, 72, 1, 3, '2026-04-29', '10:30:00', 'BOOKED'), -(58, 8, 94, 73, 2, 8, '2026-05-01', '13:00:00', 'BOOKED'), -(59, 4, 95, 74, 3, 13, '2026-05-03', '14:30:00', 'BOOKED'), -(60, 5, 96, 75, 1, 6, '2026-05-05', '16:00:00', 'BOOKED'), -(61, 1, 97, 76, 2, 7, '2026-01-07', '09:00:00', 'COMPLETED'), -(62, 2, 98, 77, 3, 12, '2026-01-09', '10:30:00', 'COMPLETED'), -(63, 3, 99, 78, 1, 5, '2026-01-11', '13:00:00', 'MISSED'), -(64, 7, 100, 79, 2, 10, '2026-01-13', '14:30:00', 'CANCELLED'), -(65, 2, 37, 16, 1, 3, '2026-01-15', '16:00:00', 'COMPLETED'), -(66, 8, 38, 17, 2, 8, '2026-01-17', '09:00:00', 'COMPLETED'), -(67, 4, 39, 18, 3, 13, '2026-01-19', '10:30:00', 'COMPLETED'), -(68, 8, 40, 19, 1, 6, '2026-01-21', '13:00:00', 'MISSED'), -(69, 4, 41, 20, 2, 7, '2026-01-23', '14:30:00', 'CANCELLED'), -(70, 8, 42, 21, 3, 12, '2026-01-25', '16:00:00', 'COMPLETED'), -(71, 1, 43, 22, 1, 5, '2026-01-27', '09:00:00', 'COMPLETED'), -(72, 8, 44, 23, 2, 10, '2026-01-29', '10:30:00', 'COMPLETED'), -(73, 4, 45, 24, 3, 11, '2026-01-31', '13:00:00', 'MISSED'), -(74, 8, 46, 25, 1, 4, '2026-02-02', '14:30:00', 'CANCELLED'), -(75, 6, 47, 26, 2, 9, '2026-02-04', '16:00:00', 'COMPLETED'), -(76, 7, 48, 27, 3, 14, '2026-02-06', '09:00:00', 'COMPLETED'), -(77, 2, 49, 28, 1, 3, '2026-02-08', '10:30:00', 'COMPLETED'), -(78, 4, 50, 29, 2, 8, '2026-02-10', '13:00:00', 'MISSED'), -(79, 4, 51, 30, 3, 13, '2026-02-12', '14:30:00', 'CANCELLED'), -(80, 5, 52, 31, 1, 6, '2026-02-14', '16:00:00', 'COMPLETED'), -(81, 1, 53, 32, 2, 7, '2026-02-16', '09:00:00', 'COMPLETED'), -(82, 2, 54, 33, 3, 12, '2026-02-18', '10:30:00', 'COMPLETED'), -(83, 3, 55, 34, 2, 9, '2026-02-20', '13:00:00', 'MISSED'), -(84, 4, 56, 35, 3, 14, '2026-02-22', '14:30:00', 'CANCELLED'), -(85, 5, 57, 36, 1, 3, '2026-02-24', '16:00:00', 'COMPLETED'), -(86, 4, 58, 37, 2, 8, '2026-02-26', '09:00:00', 'COMPLETED'), -(87, 4, 59, 38, 3, 13, '2026-02-28', '10:30:00', 'COMPLETED'), -(88, 2, 60, 39, 1, 6, '2026-03-02', '13:00:00', 'MISSED'), -(89, 2, 61, 40, 2, 7, '2026-03-04', '14:30:00', 'CANCELLED'), -(90, 5, 62, 41, 3, 12, '2026-03-06', '16:00:00', 'COMPLETED'); +(1, 2, 37, 16, 1, 3, '2026-01-07', '09:00:00', 'Completed'), +(2, 8, 38, 17, 2, 8, '2026-01-09', '10:30:00', 'Completed'), +(3, 4, 39, 18, 3, 13, '2026-01-11', '13:00:00', 'Missed'), +(4, 8, 40, 19, 1, 6, '2026-01-13', '14:30:00', 'Cancelled'), +(5, 5, 41, 20, 2, 7, '2026-01-15', '16:00:00', 'Completed'), +(6, 8, 42, 21, 3, 12, '2026-01-17', '09:00:00', 'Completed'), +(7, 2, 43, 22, 1, 5, '2026-01-19', '10:30:00', 'Completed'), +(8, 8, 44, 23, 2, 10, '2026-01-21', '13:00:00', 'Missed'), +(9, 4, 45, 24, 3, 11, '2026-01-23', '14:30:00', 'Cancelled'), +(10, 8, 46, 25, 1, 4, '2026-01-25', '16:00:00', 'Completed'), +(11, 6, 47, 26, 2, 9, '2026-01-27', '09:00:00', 'Completed'), +(12, 7, 48, 27, 3, 14, '2026-01-29', '10:30:00', 'Completed'), +(13, 2, 49, 28, 1, 3, '2026-01-31', '13:00:00', 'Missed'), +(14, 4, 50, 29, 2, 8, '2026-02-02', '14:30:00', 'Cancelled'), +(15, 5, 51, 30, 3, 13, '2026-02-04', '16:00:00', 'Completed'), +(16, 1, 52, 31, 1, 6, '2026-02-06', '09:00:00', 'Completed'), +(17, 2, 53, 32, 2, 7, '2026-02-08', '10:30:00', 'Completed'), +(18, 3, 54, 33, 3, 12, '2026-02-10', '13:00:00', 'Missed'), +(19, 4, 55, 34, 2, 9, '2026-02-12', '14:30:00', 'Cancelled'), +(20, 5, 56, 35, 3, 14, '2026-02-14', '16:00:00', 'Completed'), +(21, 1, 57, 36, 1, 3, '2026-02-16', '09:00:00', 'Completed'), +(22, 4, 58, 37, 2, 8, '2026-02-18', '10:30:00', 'Completed'), +(23, 4, 59, 38, 3, 13, '2026-02-20', '13:00:00', 'Missed'), +(24, 5, 60, 39, 1, 6, '2026-02-22', '14:30:00', 'Cancelled'), +(25, 2, 61, 40, 2, 7, '2026-02-24', '16:00:00', 'Completed'), +(26, 1, 62, 41, 3, 12, '2026-02-26', '09:00:00', 'Completed'), +(27, 2, 63, 42, 1, 5, '2026-02-28', '10:30:00', 'Completed'), +(28, 3, 64, 43, 2, 10, '2026-03-02', '13:00:00', 'Missed'), +(29, 2, 65, 44, 3, 11, '2026-03-04', '14:30:00', 'Cancelled'), +(30, 8, 66, 45, 1, 4, '2026-03-06', '16:00:00', 'Completed'), +(31, 2, 67, 46, 2, 9, '2026-03-08', '09:00:00', 'Completed'), +(32, 5, 68, 47, 3, 14, '2026-03-10', '10:30:00', 'Completed'), +(33, 3, 69, 48, 1, 3, '2026-03-12', '13:00:00', 'Missed'), +(34, 4, 70, 49, 2, 8, '2026-03-14', '14:30:00', 'Cancelled'), +(35, 5, 71, 50, 3, 13, '2026-03-16', '16:00:00', 'Completed'), +(36, 7, 72, 51, 1, 6, '2026-03-18', '09:00:00', 'Completed'), +(37, 4, 73, 52, 2, 7, '2026-03-20', '10:30:00', 'Completed'), +(38, 4, 74, 53, 3, 12, '2026-03-22', '13:00:00', 'Missed'), +(39, 4, 75, 54, 1, 5, '2026-03-24', '14:30:00', 'Cancelled'), +(40, 5, 76, 55, 2, 10, '2026-03-26', '16:00:00', 'Completed'), +(41, 1, 77, 56, 3, 11, '2026-03-28', '09:00:00', 'Completed'), +(42, 2, 78, 57, 1, 4, '2026-03-30', '10:30:00', 'Completed'), +(43, 6, 79, 58, 2, 9, '2026-04-01', '13:00:00', 'Booked'), +(44, 8, 80, 59, 3, 14, '2026-04-03', '14:30:00', 'Booked'), +(45, 5, 81, 60, 1, 3, '2026-04-05', '16:00:00', 'Booked'), +(46, 3, 82, 61, 2, 8, '2026-04-07', '09:00:00', 'Booked'), +(47, 2, 83, 62, 3, 13, '2026-04-09', '10:30:00', 'Booked'), +(48, 3, 84, 63, 1, 6, '2026-04-11', '13:00:00', 'Booked'), +(49, 4, 85, 64, 2, 7, '2026-04-13', '14:30:00', 'Booked'), +(50, 4, 86, 65, 3, 12, '2026-04-15', '16:00:00', 'Booked'), +(51, 4, 87, 66, 1, 5, '2026-04-17', '09:00:00', 'Booked'), +(52, 2, 88, 67, 2, 10, '2026-04-19', '10:30:00', 'Booked'), +(53, 2, 89, 68, 3, 11, '2026-04-21', '13:00:00', 'Booked'), +(54, 4, 90, 69, 1, 4, '2026-04-23', '14:30:00', 'Booked'), +(55, 5, 91, 70, 2, 9, '2026-04-25', '16:00:00', 'Booked'), +(56, 1, 92, 71, 3, 14, '2026-04-27', '09:00:00', 'Booked'), +(57, 2, 93, 72, 1, 3, '2026-04-29', '10:30:00', 'Booked'), +(58, 8, 94, 73, 2, 8, '2026-05-01', '13:00:00', 'Booked'), +(59, 4, 95, 74, 3, 13, '2026-05-03', '14:30:00', 'Booked'), +(60, 5, 96, 75, 1, 6, '2026-05-05', '16:00:00', 'Booked'), +(61, 1, 97, 76, 2, 7, '2026-01-07', '09:00:00', 'Completed'), +(62, 2, 98, 77, 3, 12, '2026-01-09', '10:30:00', 'Completed'), +(63, 3, 99, 78, 1, 5, '2026-01-11', '13:00:00', 'Missed'), +(64, 7, 100, 79, 2, 10, '2026-01-13', '14:30:00', 'Cancelled'), +(65, 2, 37, 16, 1, 3, '2026-01-15', '16:00:00', 'Completed'), +(66, 8, 38, 17, 2, 8, '2026-01-17', '09:00:00', 'Completed'), +(67, 4, 39, 18, 3, 13, '2026-01-19', '10:30:00', 'Completed'), +(68, 8, 40, 19, 1, 6, '2026-01-21', '13:00:00', 'Missed'), +(69, 4, 41, 20, 2, 7, '2026-01-23', '14:30:00', 'Cancelled'), +(70, 8, 42, 21, 3, 12, '2026-01-25', '16:00:00', 'Completed'), +(71, 1, 43, 22, 1, 5, '2026-01-27', '09:00:00', 'Completed'), +(72, 8, 44, 23, 2, 10, '2026-01-29', '10:30:00', 'Completed'), +(73, 4, 45, 24, 3, 11, '2026-01-31', '13:00:00', 'Missed'), +(74, 8, 46, 25, 1, 4, '2026-02-02', '14:30:00', 'Cancelled'), +(75, 6, 47, 26, 2, 9, '2026-02-04', '16:00:00', 'Completed'), +(76, 7, 48, 27, 3, 14, '2026-02-06', '09:00:00', 'Completed'), +(77, 2, 49, 28, 1, 3, '2026-02-08', '10:30:00', 'Completed'), +(78, 4, 50, 29, 2, 8, '2026-02-10', '13:00:00', 'Missed'), +(79, 4, 51, 30, 3, 13, '2026-02-12', '14:30:00', 'Cancelled'), +(80, 5, 52, 31, 1, 6, '2026-02-14', '16:00:00', 'Completed'), +(81, 1, 53, 32, 2, 7, '2026-02-16', '09:00:00', 'Completed'), +(82, 2, 54, 33, 3, 12, '2026-02-18', '10:30:00', 'Completed'), +(83, 3, 55, 34, 2, 9, '2026-02-20', '13:00:00', 'Missed'), +(84, 4, 56, 35, 3, 14, '2026-02-22', '14:30:00', 'Cancelled'), +(85, 5, 57, 36, 1, 3, '2026-02-24', '16:00:00', 'Completed'), +(86, 4, 58, 37, 2, 8, '2026-02-26', '09:00:00', 'Completed'), +(87, 4, 59, 38, 3, 13, '2026-02-28', '10:30:00', 'Completed'), +(88, 2, 60, 39, 1, 6, '2026-03-02', '13:00:00', 'Missed'), +(89, 2, 61, 40, 2, 7, '2026-03-04', '14:30:00', 'Cancelled'), +(90, 5, 62, 41, 3, 12, '2026-03-06', '16:00:00', 'Completed'); INSERT INTO adoption (adoptionId, petId, customerId, employeeId, sourceStoreId, adoptionDate, adoptionStatus) VALUES (1, 37, 16, 3, 1, '2026-01-08', 'Completed'), @@ -1217,116 +1217,116 @@ INSERT INTO cart_item (cartItemId, cartId, prodId, quantity, unitPrice) VALUES (119, 40, 85, 2, 46.43); INSERT INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES -(1, '2026-02-02 10:11:00', 37.83, 'Cash', 3, 1, 3, 0, NULL, 'ONLINE', 1, 1, 44.51, 0.00, 6.68, 0), -(2, '2026-02-03 10:22:00', 192.78, 'Card', 4, 1, 4, 0, NULL, 'ONLINE', 2, 2, 252.00, 25.20, 34.02, 0), -(3, '2026-02-04 10:33:00', 363.23, 'Card', 5, 1, 5, 0, NULL, 'ONLINE', 3, 3, 432.33, 5.00, 64.10, 0), -(4, '2026-02-05 10:44:00', 81.29, 'Cash', 6, 1, 6, 0, NULL, 'ONLINE', 4, 7, 108.67, 13.04, 14.34, 0), -(5, '2026-02-06 10:55:00', 435.73, 'Card', 7, 2, 7, 0, NULL, 'ONLINE', 5, 1, 512.62, 0.00, 76.89, 0), -(6, '2026-02-07 11:06:00', 409.56, 'Card', 8, 2, 8, 0, NULL, 'ONLINE', 6, 1, 481.83, 0.00, 72.27, 0), -(7, '2026-02-08 11:17:00', 56.40, 'Cash', 9, 2, 9, 0, NULL, 'ONLINE', 7, 4, 78.06, 11.71, 9.95, 0), -(8, '2026-02-09 11:28:00', 174.20, 'Card', 10, 2, 10, 0, NULL, 'ONLINE', 8, 5, 212.94, 8.00, 30.74, 0), -(9, '2026-02-10 11:39:00', 619.91, 'Card', 11, 3, 11, 0, NULL, 'ONLINE', 9, 1, 729.31, 0.00, 109.40, 0), -(10, '2026-02-11 11:50:00', 169.73, 'Card', 12, 3, 12, 0, NULL, 'ONLINE', 10, 6, 221.87, 22.19, 29.95, 0), -(11, '2026-02-12 12:01:00', 137.86, 'Cash', 13, 3, 13, 0, NULL, 'ONLINE', 11, 1, 162.19, 0.00, 24.33, 0), -(12, '2026-02-13 12:12:00', 453.95, 'Card', 14, 3, 14, 0, NULL, 'ONLINE', 12, 2, 593.40, 59.34, 80.11, 0), -(13, '2026-01-05 09:15:00', 82.72, 'Card', 3, 1, 15, 0, NULL, 'IN_STORE', NULL, 1, 82.72, 0.00, 0.00, 0), -(14, '2026-01-05 09:52:00', 120.43, 'Card', 8, 2, 16, 0, NULL, 'IN_STORE', NULL, 2, 133.81, 13.38, 0.00, 12), -(15, '2026-01-06 10:29:00', 153.21, 'Cash', 13, 3, 17, 0, NULL, 'IN_STORE', NULL, 1, 153.21, 0.00, 0.00, 15), -(16, '2026-01-06 11:06:00', 20.27, 'Card', 6, 1, 18, 0, NULL, 'IN_STORE', NULL, 3, 25.27, 5.00, 0.00, 2), -(17, '2026-01-07 11:43:00', 58.96, 'Cash', 7, 2, 19, 0, NULL, 'IN_STORE', NULL, 1, 58.96, 0.00, 0.00, 5), -(18, '2026-01-07 12:20:00', 124.54, 'Card', 12, 3, 20, 0, NULL, 'IN_STORE', NULL, 7, 141.52, 16.98, 0.00, 12), -(19, '2026-01-08 12:57:00', 118.84, 'Card', 5, 1, 21, 0, NULL, 'IN_STORE', NULL, 1, 118.84, 0.00, 0.00, 11), -(20, '2026-01-08 13:34:00', 167.02, 'Cash', 10, 2, 22, 0, NULL, 'IN_STORE', NULL, 4, 196.50, 29.48, 0.00, 16), -(21, '2026-01-09 14:11:00', 367.69, 'Card', 11, 3, 23, 0, NULL, 'IN_STORE', NULL, 1, 367.69, 0.00, 0.00, 36), -(22, '2026-01-09 14:48:00', 57.62, 'Cash', 4, 1, 24, 0, NULL, 'IN_STORE', NULL, 1, 57.62, 0.00, 0.00, 5), -(23, '2026-01-10 15:25:00', 84.03, 'Card', 9, 2, 25, 0, NULL, 'IN_STORE', NULL, 6, 93.37, 9.34, 0.00, 8), -(24, '2026-01-10 16:02:00', 297.25, 'Card', 14, 3, 26, 0, NULL, 'IN_STORE', NULL, 1, 297.25, 0.00, 0.00, 29), -(25, '2026-01-11 16:39:00', 35.78, 'Cash', 3, 1, 27, 0, NULL, 'IN_STORE', NULL, 2, 35.78, 0.00, 0.00, 3), -(26, '2026-01-11 17:16:00', 136.99, 'Card', 8, 2, 28, 0, NULL, 'IN_STORE', NULL, 1, 136.99, 0.00, 0.00, 13), -(27, '2026-01-12 17:53:00', 300.52, 'Cash', 13, 3, 29, 0, NULL, 'IN_STORE', NULL, 3, 305.52, 5.00, 0.00, 30), -(28, '2026-01-12 18:30:00', 165.99, 'Card', 6, 1, 30, 0, NULL, 'IN_STORE', NULL, 1, 165.99, 0.00, 0.00, 16), -(29, '2026-01-13 19:07:00', 91.28, 'Card', 7, 2, 31, 0, NULL, 'IN_STORE', NULL, 7, 103.73, 12.45, 0.00, 9), -(30, '2026-01-13 19:44:00', 198.88, 'Cash', 12, 3, 32, 0, NULL, 'IN_STORE', NULL, 1, 198.88, 0.00, 0.00, 19), -(31, '2026-01-14 20:21:00', 25.28, 'Card', 5, 1, 33, 0, NULL, 'IN_STORE', NULL, 4, 25.28, 0.00, 0.00, 2), -(32, '2026-01-14 20:58:00', 58.51, 'Cash', 10, 2, 34, 0, NULL, 'IN_STORE', NULL, 1, 58.51, 0.00, 0.00, 5), -(33, '2026-01-15 21:35:00', 314.15, 'Card', 11, 3, 35, 0, NULL, 'IN_STORE', NULL, 1, 314.15, 0.00, 0.00, 31), -(34, '2026-01-15 22:12:00', 61.62, 'Card', 4, 1, 36, 0, NULL, 'IN_STORE', NULL, 6, 68.47, 6.85, 0.00, 6), -(35, '2026-01-16 22:49:00', 49.61, 'Cash', 9, 2, 37, 0, NULL, 'IN_STORE', NULL, 1, 49.61, 0.00, 0.00, 4), -(36, '2026-01-16 23:26:00', 196.32, 'Card', 14, 3, 38, 0, NULL, 'IN_STORE', NULL, 2, 218.13, 21.81, 0.00, 19), -(37, '2026-01-18 00:03:00', 47.92, 'Cash', 3, 1, 39, 0, NULL, 'IN_STORE', NULL, 1, 47.92, 0.00, 0.00, 4), -(38, '2026-01-18 00:40:00', 121.03, 'Card', 8, 2, 40, 0, NULL, 'IN_STORE', NULL, 3, 126.03, 5.00, 0.00, 12), -(39, '2026-01-19 01:17:00', 187.91, 'Card', 13, 3, 41, 0, NULL, 'IN_STORE', NULL, 1, 187.91, 0.00, 0.00, 18), -(40, '2026-01-19 01:54:00', 108.22, 'Cash', 6, 1, 42, 0, NULL, 'IN_STORE', NULL, 7, 122.98, 14.76, 0.00, 10), -(41, '2026-01-20 02:31:00', 67.71, 'Card', 7, 2, 43, 0, NULL, 'IN_STORE', NULL, 1, 67.71, 0.00, 0.00, 6), -(42, '2026-01-20 03:08:00', 114.93, 'Cash', 12, 3, 44, 0, NULL, 'IN_STORE', NULL, 4, 135.21, 20.28, 0.00, 11), -(43, '2026-01-21 03:45:00', 55.38, 'Card', 5, 1, 45, 0, NULL, 'IN_STORE', NULL, 1, 55.38, 0.00, 0.00, 5), -(44, '2026-01-21 04:22:00', 286.34, 'Card', 10, 2, 46, 0, NULL, 'IN_STORE', NULL, 1, 286.34, 0.00, 0.00, 28), -(45, '2026-01-22 04:59:00', 83.62, 'Cash', 11, 3, 47, 0, NULL, 'IN_STORE', NULL, 6, 92.91, 9.29, 0.00, 8), -(46, '2026-01-22 05:36:00', 29.89, 'Card', 4, 1, 48, 0, NULL, 'IN_STORE', NULL, 1, 29.89, 0.00, 0.00, 2), -(47, '2026-01-23 06:13:00', 161.48, 'Cash', 9, 2, 49, 0, NULL, 'IN_STORE', NULL, 2, 179.42, 17.94, 0.00, 16), -(48, '2026-01-23 06:50:00', 210.14, 'Card', 14, 3, 50, 0, NULL, 'IN_STORE', NULL, 1, 210.14, 0.00, 0.00, 21), -(49, '2026-01-24 07:27:00', 73.64, 'Card', 3, 1, 51, 0, NULL, 'IN_STORE', NULL, 3, 78.64, 5.00, 0.00, 7), -(50, '2026-01-24 08:04:00', 179.28, 'Cash', 8, 2, 52, 0, NULL, 'IN_STORE', NULL, 1, 179.28, 0.00, 0.00, 17), -(51, '2026-01-25 08:41:00', 101.67, 'Card', 13, 3, 53, 0, NULL, 'IN_STORE', NULL, 7, 115.53, 13.86, 0.00, 10), -(52, '2026-01-25 09:18:00', 21.31, 'Cash', 6, 1, 54, 0, NULL, 'IN_STORE', NULL, 1, 21.31, 0.00, 0.00, 2), -(53, '2026-01-26 09:55:00', 79.57, 'Card', 7, 2, 55, 0, NULL, 'IN_STORE', NULL, 4, 93.61, 14.04, 0.00, 7), -(54, '2026-01-26 10:32:00', 156.49, 'Card', 12, 3, 56, 0, NULL, 'IN_STORE', NULL, 1, 156.49, 0.00, 0.00, 15), -(55, '2026-01-27 11:09:00', 28.32, 'Cash', 5, 1, 57, 0, NULL, 'IN_STORE', NULL, 1, 28.32, 0.00, 0.00, 2), -(56, '2026-01-27 11:46:00', 175.47, 'Card', 10, 2, 58, 0, NULL, 'IN_STORE', NULL, 6, 194.97, 19.50, 0.00, 17), -(57, '2026-01-28 12:23:00', 150.60, 'Cash', 11, 3, 59, 0, NULL, 'IN_STORE', NULL, 1, 150.60, 0.00, 0.00, 15), -(58, '2026-01-28 13:00:00', 45.73, 'Card', 4, 1, 60, 0, NULL, 'IN_STORE', NULL, 2, 45.73, 0.00, 0.00, 4), -(59, '2026-01-29 13:37:00', 132.93, 'Card', 9, 2, 61, 0, NULL, 'IN_STORE', NULL, 1, 132.93, 0.00, 0.00, 13), -(60, '2026-01-29 14:14:00', 261.49, 'Cash', 14, 3, 62, 0, NULL, 'IN_STORE', NULL, 3, 266.49, 5.00, 0.00, 26), -(61, '2026-01-30 14:51:00', 57.02, 'Card', 3, 1, 63, 0, NULL, 'IN_STORE', NULL, 1, 57.02, 0.00, 0.00, 5), -(62, '2026-01-30 15:28:00', 90.64, 'Cash', 8, 2, 64, 0, NULL, 'IN_STORE', NULL, 7, 103.00, 12.36, 0.00, 9), -(63, '2026-01-31 16:05:00', 228.91, 'Card', 13, 3, 65, 0, NULL, 'IN_STORE', NULL, 1, 228.91, 0.00, 0.00, 22), -(64, '2026-01-31 16:42:00', 50.36, 'Card', 6, 1, 66, 0, NULL, 'IN_STORE', NULL, 4, 50.36, 0.00, 0.00, 5), -(65, '2026-02-01 17:19:00', 53.77, 'Cash', 7, 2, 67, 0, NULL, 'IN_STORE', NULL, 1, 53.77, 0.00, 0.00, 5), -(66, '2026-02-01 17:56:00', 172.92, 'Card', 12, 3, 68, 0, NULL, 'IN_STORE', NULL, 1, 172.92, 0.00, 0.00, 17), -(67, '2026-02-02 18:33:00', 154.78, 'Cash', 5, 1, 69, 0, NULL, 'IN_STORE', NULL, 6, 171.98, 17.20, 0.00, 15), -(68, '2026-02-02 19:10:00', 198.21, 'Card', 10, 2, 70, 0, NULL, 'IN_STORE', NULL, 1, 198.21, 0.00, 0.00, 19), -(69, '2026-02-03 19:47:00', 134.85, 'Card', 11, 3, 71, 0, NULL, 'IN_STORE', NULL, 2, 149.83, 14.98, 0.00, 13), -(70, '2026-02-03 20:24:00', 74.88, 'Cash', 4, 1, 72, 0, NULL, 'IN_STORE', NULL, 1, 74.88, 0.00, 0.00, 7), -(71, '2026-02-04 21:01:00', 27.77, 'Card', 9, 2, 73, 0, NULL, 'IN_STORE', NULL, 3, 32.77, 5.00, 0.00, 2), -(72, '2026-02-04 21:38:00', 105.22, 'Cash', 14, 3, 74, 0, NULL, 'IN_STORE', NULL, 1, 105.22, 0.00, 0.00, 10), -(73, '2026-02-05 22:15:00', 72.79, 'Card', 3, 1, 75, 0, NULL, 'IN_STORE', NULL, 7, 82.72, 9.93, 0.00, 7), -(74, '2026-02-05 22:52:00', 133.81, 'Card', 8, 2, 76, 0, NULL, 'IN_STORE', NULL, 1, 133.81, 0.00, 0.00, 13), -(75, '2026-02-06 23:29:00', 130.23, 'Cash', 13, 3, 77, 0, NULL, 'IN_STORE', NULL, 4, 153.21, 22.98, 0.00, 13), -(76, '2026-02-07 00:06:00', 25.27, 'Card', 6, 1, 78, 0, NULL, 'IN_STORE', NULL, 1, 25.27, 0.00, 0.00, 2), -(77, '2026-02-08 00:43:00', 58.96, 'Cash', 7, 2, 79, 0, NULL, 'IN_STORE', NULL, 1, 58.96, 0.00, 0.00, 5), -(78, '2026-02-08 01:20:00', 127.37, 'Card', 12, 3, 80, 0, NULL, 'IN_STORE', NULL, 6, 141.52, 14.15, 0.00, 12), -(79, '2026-02-09 01:57:00', 118.84, 'Card', 5, 1, 81, 0, NULL, 'IN_STORE', NULL, 1, 118.84, 0.00, 0.00, 11), -(80, '2026-02-09 02:34:00', 176.85, 'Cash', 10, 2, 82, 0, NULL, 'IN_STORE', NULL, 2, 196.50, 19.65, 0.00, 17), -(81, '2026-02-10 03:11:00', 367.69, 'Card', 11, 3, 83, 0, NULL, 'IN_STORE', NULL, 1, 367.69, 0.00, 0.00, 36), -(82, '2026-02-10 03:48:00', 52.62, 'Cash', 4, 1, 84, 0, NULL, 'IN_STORE', NULL, 3, 57.62, 5.00, 0.00, 5), -(83, '2026-02-11 04:25:00', 93.37, 'Card', 9, 2, 85, 0, NULL, 'IN_STORE', NULL, 1, 93.37, 0.00, 0.00, 9), -(84, '2026-02-11 05:02:00', 261.58, 'Card', 14, 3, 86, 0, NULL, 'IN_STORE', NULL, 7, 297.25, 35.67, 0.00, 26), -(85, '2026-02-12 05:39:00', 35.78, 'Cash', 3, 1, 87, 0, NULL, 'IN_STORE', NULL, 1, 35.78, 0.00, 0.00, 3), -(86, '2026-02-12 06:16:00', 116.44, 'Card', 8, 2, 88, 0, NULL, 'IN_STORE', NULL, 4, 136.99, 20.55, 0.00, 11), -(87, '2026-02-13 06:53:00', 305.52, 'Cash', 13, 3, 89, 0, NULL, 'IN_STORE', NULL, 1, 305.52, 0.00, 0.00, 30), -(88, '2026-02-13 07:30:00', 165.99, 'Card', 6, 1, 90, 0, NULL, 'IN_STORE', NULL, 1, 165.99, 0.00, 0.00, 16), -(89, '2026-02-14 08:07:00', 93.36, 'Card', 7, 2, 91, 0, NULL, 'IN_STORE', NULL, 6, 103.73, 10.37, 0.00, 9), -(90, '2026-02-14 08:44:00', 198.88, 'Cash', 12, 3, 92, 0, NULL, 'IN_STORE', NULL, 1, 198.88, 0.00, 0.00, 19), -(91, '2026-02-15 09:21:00', 25.28, 'Card', 5, 1, 93, 0, NULL, 'IN_STORE', NULL, 2, 25.28, 0.00, 0.00, 2), -(92, '2026-02-15 09:58:00', 58.51, 'Cash', 10, 2, 94, 0, NULL, 'IN_STORE', NULL, 1, 58.51, 0.00, 0.00, 5), -(93, '2026-02-16 10:35:00', 309.15, 'Card', 11, 3, 95, 0, NULL, 'IN_STORE', NULL, 3, 314.15, 5.00, 0.00, 30), -(94, '2026-02-16 11:12:00', 68.47, 'Card', 4, 1, 96, 0, NULL, 'IN_STORE', NULL, 1, 68.47, 0.00, 0.00, 6), -(95, '2026-02-17 11:49:00', 49.61, 'Cash', 9, 2, 97, 0, NULL, 'IN_STORE', NULL, 7, 49.61, 0.00, 0.00, 4), -(96, '2026-02-13 10:11:00', 34.80, 'Card', 3, 1, 3, 1, 1, 'IN_STORE', NULL, 1, 34.80, 0.00, 0.00, 0), -(97, '2026-02-15 10:22:00', 88.32, 'Card', 4, 1, 4, 1, 2, 'IN_STORE', NULL, 1, 88.32, 0.00, 0.00, 0), -(98, '2026-02-17 10:33:00', 49.05, 'Card', 5, 1, 5, 1, 3, 'IN_STORE', NULL, 1, 49.05, 0.00, 0.00, 0), -(99, '2026-02-19 10:44:00', 15.25, 'Card', 6, 1, 6, 1, 4, 'IN_STORE', NULL, 1, 15.25, 0.00, 0.00, 0), -(100, '2026-02-21 10:55:00', 181.42, 'Card', 7, 2, 7, 1, 5, 'IN_STORE', NULL, 1, 181.42, 0.00, 0.00, 0), -(101, '2026-02-23 11:06:00', 130.82, 'Card', 8, 2, 8, 1, 6, 'IN_STORE', NULL, 1, 130.82, 0.00, 0.00, 0), -(102, '2026-02-25 11:17:00', 50.37, 'Card', 9, 2, 9, 1, 7, 'IN_STORE', NULL, 1, 50.37, 0.00, 0.00, 0), -(103, '2026-02-27 11:28:00', 74.13, 'Card', 10, 2, 10, 1, 8, 'IN_STORE', NULL, 1, 74.13, 0.00, 0.00, 0), -(104, '2026-03-01 11:39:00', 68.78, 'Card', 11, 3, 11, 1, 9, 'IN_STORE', NULL, 1, 68.78, 0.00, 0.00, 0), -(105, '2026-03-03 11:50:00', 17.89, 'Card', 12, 3, 12, 1, 10, 'IN_STORE', NULL, 1, 17.89, 0.00, 0.00, 0), -(106, '2026-03-05 12:01:00', 63.09, 'Card', 13, 3, 13, 1, 11, 'IN_STORE', NULL, 1, 63.09, 0.00, 0.00, 0), -(107, '2026-03-07 12:12:00', 149.99, 'Card', 14, 3, 14, 1, 12, 'IN_STORE', NULL, 1, 149.99, 0.00, 0.00, 0), -(108, '2026-01-28 09:15:00', 41.36, 'Card', 3, 1, 15, 1, 13, 'IN_STORE', NULL, 1, 41.36, 0.00, 0.00, 0), -(109, '2026-01-29 09:52:00', 68.47, 'Card', 8, 2, 16, 1, 14, 'IN_STORE', NULL, 1, 68.47, 0.00, 0.00, 0), -(110, '2026-01-31 10:29:00', 14.16, 'Card', 13, 3, 17, 1, 15, 'IN_STORE', NULL, 1, 14.16, 0.00, 0.00, 0); +(1, '2026-02-02 10:11:00', 37.83, 'Cash', 3, 1, 3, 0, NULL, 'Online', 1, 1, 44.51, 0.00, 6.68, 0), +(2, '2026-02-03 10:22:00', 192.78, 'Card', 4, 1, 4, 0, NULL, 'Online', 2, 2, 252.00, 25.20, 34.02, 0), +(3, '2026-02-04 10:33:00', 363.23, 'Card', 5, 1, 5, 0, NULL, 'Online', 3, 3, 432.33, 5.00, 64.10, 0), +(4, '2026-02-05 10:44:00', 81.29, 'Cash', 6, 1, 6, 0, NULL, 'Online', 4, 7, 108.67, 13.04, 14.34, 0), +(5, '2026-02-06 10:55:00', 435.73, 'Card', 7, 2, 7, 0, NULL, 'Online', 5, 1, 512.62, 0.00, 76.89, 0), +(6, '2026-02-07 11:06:00', 409.56, 'Card', 8, 2, 8, 0, NULL, 'Online', 6, 1, 481.83, 0.00, 72.27, 0), +(7, '2026-02-08 11:17:00', 56.40, 'Cash', 9, 2, 9, 0, NULL, 'Online', 7, 4, 78.06, 11.71, 9.95, 0), +(8, '2026-02-09 11:28:00', 174.20, 'Card', 10, 2, 10, 0, NULL, 'Online', 8, 5, 212.94, 8.00, 30.74, 0), +(9, '2026-02-10 11:39:00', 619.91, 'Card', 11, 3, 11, 0, NULL, 'Online', 9, 1, 729.31, 0.00, 109.40, 0), +(10, '2026-02-11 11:50:00', 169.73, 'Card', 12, 3, 12, 0, NULL, 'Online', 10, 6, 221.87, 22.19, 29.95, 0), +(11, '2026-02-12 12:01:00', 137.86, 'Cash', 13, 3, 13, 0, NULL, 'Online', 11, 1, 162.19, 0.00, 24.33, 0), +(12, '2026-02-13 12:12:00', 453.95, 'Card', 14, 3, 14, 0, NULL, 'Online', 12, 2, 593.40, 59.34, 80.11, 0), +(13, '2026-01-05 09:15:00', 82.72, 'Card', 3, 1, 15, 0, NULL, 'In Store', NULL, 1, 82.72, 0.00, 0.00, 0), +(14, '2026-01-05 09:52:00', 120.43, 'Card', 8, 2, 16, 0, NULL, 'In Store', NULL, 2, 133.81, 13.38, 0.00, 12), +(15, '2026-01-06 10:29:00', 153.21, 'Cash', 13, 3, 17, 0, NULL, 'In Store', NULL, 1, 153.21, 0.00, 0.00, 15), +(16, '2026-01-06 11:06:00', 20.27, 'Card', 6, 1, 18, 0, NULL, 'In Store', NULL, 3, 25.27, 5.00, 0.00, 2), +(17, '2026-01-07 11:43:00', 58.96, 'Cash', 7, 2, 19, 0, NULL, 'In Store', NULL, 1, 58.96, 0.00, 0.00, 5), +(18, '2026-01-07 12:20:00', 124.54, 'Card', 12, 3, 20, 0, NULL, 'In Store', NULL, 7, 141.52, 16.98, 0.00, 12), +(19, '2026-01-08 12:57:00', 118.84, 'Card', 5, 1, 21, 0, NULL, 'In Store', NULL, 1, 118.84, 0.00, 0.00, 11), +(20, '2026-01-08 13:34:00', 167.02, 'Cash', 10, 2, 22, 0, NULL, 'In Store', NULL, 4, 196.50, 29.48, 0.00, 16), +(21, '2026-01-09 14:11:00', 367.69, 'Card', 11, 3, 23, 0, NULL, 'In Store', NULL, 1, 367.69, 0.00, 0.00, 36), +(22, '2026-01-09 14:48:00', 57.62, 'Cash', 4, 1, 24, 0, NULL, 'In Store', NULL, 1, 57.62, 0.00, 0.00, 5), +(23, '2026-01-10 15:25:00', 84.03, 'Card', 9, 2, 25, 0, NULL, 'In Store', NULL, 6, 93.37, 9.34, 0.00, 8), +(24, '2026-01-10 16:02:00', 297.25, 'Card', 14, 3, 26, 0, NULL, 'In Store', NULL, 1, 297.25, 0.00, 0.00, 29), +(25, '2026-01-11 16:39:00', 35.78, 'Cash', 3, 1, 27, 0, NULL, 'In Store', NULL, 2, 35.78, 0.00, 0.00, 3), +(26, '2026-01-11 17:16:00', 136.99, 'Card', 8, 2, 28, 0, NULL, 'In Store', NULL, 1, 136.99, 0.00, 0.00, 13), +(27, '2026-01-12 17:53:00', 300.52, 'Cash', 13, 3, 29, 0, NULL, 'In Store', NULL, 3, 305.52, 5.00, 0.00, 30), +(28, '2026-01-12 18:30:00', 165.99, 'Card', 6, 1, 30, 0, NULL, 'In Store', NULL, 1, 165.99, 0.00, 0.00, 16), +(29, '2026-01-13 19:07:00', 91.28, 'Card', 7, 2, 31, 0, NULL, 'In Store', NULL, 7, 103.73, 12.45, 0.00, 9), +(30, '2026-01-13 19:44:00', 198.88, 'Cash', 12, 3, 32, 0, NULL, 'In Store', NULL, 1, 198.88, 0.00, 0.00, 19), +(31, '2026-01-14 20:21:00', 25.28, 'Card', 5, 1, 33, 0, NULL, 'In Store', NULL, 4, 25.28, 0.00, 0.00, 2), +(32, '2026-01-14 20:58:00', 58.51, 'Cash', 10, 2, 34, 0, NULL, 'In Store', NULL, 1, 58.51, 0.00, 0.00, 5), +(33, '2026-01-15 21:35:00', 314.15, 'Card', 11, 3, 35, 0, NULL, 'In Store', NULL, 1, 314.15, 0.00, 0.00, 31), +(34, '2026-01-15 22:12:00', 61.62, 'Card', 4, 1, 36, 0, NULL, 'In Store', NULL, 6, 68.47, 6.85, 0.00, 6), +(35, '2026-01-16 22:49:00', 49.61, 'Cash', 9, 2, 37, 0, NULL, 'In Store', NULL, 1, 49.61, 0.00, 0.00, 4), +(36, '2026-01-16 23:26:00', 196.32, 'Card', 14, 3, 38, 0, NULL, 'In Store', NULL, 2, 218.13, 21.81, 0.00, 19), +(37, '2026-01-18 00:03:00', 47.92, 'Cash', 3, 1, 39, 0, NULL, 'In Store', NULL, 1, 47.92, 0.00, 0.00, 4), +(38, '2026-01-18 00:40:00', 121.03, 'Card', 8, 2, 40, 0, NULL, 'In Store', NULL, 3, 126.03, 5.00, 0.00, 12), +(39, '2026-01-19 01:17:00', 187.91, 'Card', 13, 3, 41, 0, NULL, 'In Store', NULL, 1, 187.91, 0.00, 0.00, 18), +(40, '2026-01-19 01:54:00', 108.22, 'Cash', 6, 1, 42, 0, NULL, 'In Store', NULL, 7, 122.98, 14.76, 0.00, 10), +(41, '2026-01-20 02:31:00', 67.71, 'Card', 7, 2, 43, 0, NULL, 'In Store', NULL, 1, 67.71, 0.00, 0.00, 6), +(42, '2026-01-20 03:08:00', 114.93, 'Cash', 12, 3, 44, 0, NULL, 'In Store', NULL, 4, 135.21, 20.28, 0.00, 11), +(43, '2026-01-21 03:45:00', 55.38, 'Card', 5, 1, 45, 0, NULL, 'In Store', NULL, 1, 55.38, 0.00, 0.00, 5), +(44, '2026-01-21 04:22:00', 286.34, 'Card', 10, 2, 46, 0, NULL, 'In Store', NULL, 1, 286.34, 0.00, 0.00, 28), +(45, '2026-01-22 04:59:00', 83.62, 'Cash', 11, 3, 47, 0, NULL, 'In Store', NULL, 6, 92.91, 9.29, 0.00, 8), +(46, '2026-01-22 05:36:00', 29.89, 'Card', 4, 1, 48, 0, NULL, 'In Store', NULL, 1, 29.89, 0.00, 0.00, 2), +(47, '2026-01-23 06:13:00', 161.48, 'Cash', 9, 2, 49, 0, NULL, 'In Store', NULL, 2, 179.42, 17.94, 0.00, 16), +(48, '2026-01-23 06:50:00', 210.14, 'Card', 14, 3, 50, 0, NULL, 'In Store', NULL, 1, 210.14, 0.00, 0.00, 21), +(49, '2026-01-24 07:27:00', 73.64, 'Card', 3, 1, 51, 0, NULL, 'In Store', NULL, 3, 78.64, 5.00, 0.00, 7), +(50, '2026-01-24 08:04:00', 179.28, 'Cash', 8, 2, 52, 0, NULL, 'In Store', NULL, 1, 179.28, 0.00, 0.00, 17), +(51, '2026-01-25 08:41:00', 101.67, 'Card', 13, 3, 53, 0, NULL, 'In Store', NULL, 7, 115.53, 13.86, 0.00, 10), +(52, '2026-01-25 09:18:00', 21.31, 'Cash', 6, 1, 54, 0, NULL, 'In Store', NULL, 1, 21.31, 0.00, 0.00, 2), +(53, '2026-01-26 09:55:00', 79.57, 'Card', 7, 2, 55, 0, NULL, 'In Store', NULL, 4, 93.61, 14.04, 0.00, 7), +(54, '2026-01-26 10:32:00', 156.49, 'Card', 12, 3, 56, 0, NULL, 'In Store', NULL, 1, 156.49, 0.00, 0.00, 15), +(55, '2026-01-27 11:09:00', 28.32, 'Cash', 5, 1, 57, 0, NULL, 'In Store', NULL, 1, 28.32, 0.00, 0.00, 2), +(56, '2026-01-27 11:46:00', 175.47, 'Card', 10, 2, 58, 0, NULL, 'In Store', NULL, 6, 194.97, 19.50, 0.00, 17), +(57, '2026-01-28 12:23:00', 150.60, 'Cash', 11, 3, 59, 0, NULL, 'In Store', NULL, 1, 150.60, 0.00, 0.00, 15), +(58, '2026-01-28 13:00:00', 45.73, 'Card', 4, 1, 60, 0, NULL, 'In Store', NULL, 2, 45.73, 0.00, 0.00, 4), +(59, '2026-01-29 13:37:00', 132.93, 'Card', 9, 2, 61, 0, NULL, 'In Store', NULL, 1, 132.93, 0.00, 0.00, 13), +(60, '2026-01-29 14:14:00', 261.49, 'Cash', 14, 3, 62, 0, NULL, 'In Store', NULL, 3, 266.49, 5.00, 0.00, 26), +(61, '2026-01-30 14:51:00', 57.02, 'Card', 3, 1, 63, 0, NULL, 'In Store', NULL, 1, 57.02, 0.00, 0.00, 5), +(62, '2026-01-30 15:28:00', 90.64, 'Cash', 8, 2, 64, 0, NULL, 'In Store', NULL, 7, 103.00, 12.36, 0.00, 9), +(63, '2026-01-31 16:05:00', 228.91, 'Card', 13, 3, 65, 0, NULL, 'In Store', NULL, 1, 228.91, 0.00, 0.00, 22), +(64, '2026-01-31 16:42:00', 50.36, 'Card', 6, 1, 66, 0, NULL, 'In Store', NULL, 4, 50.36, 0.00, 0.00, 5), +(65, '2026-02-01 17:19:00', 53.77, 'Cash', 7, 2, 67, 0, NULL, 'In Store', NULL, 1, 53.77, 0.00, 0.00, 5), +(66, '2026-02-01 17:56:00', 172.92, 'Card', 12, 3, 68, 0, NULL, 'In Store', NULL, 1, 172.92, 0.00, 0.00, 17), +(67, '2026-02-02 18:33:00', 154.78, 'Cash', 5, 1, 69, 0, NULL, 'In Store', NULL, 6, 171.98, 17.20, 0.00, 15), +(68, '2026-02-02 19:10:00', 198.21, 'Card', 10, 2, 70, 0, NULL, 'In Store', NULL, 1, 198.21, 0.00, 0.00, 19), +(69, '2026-02-03 19:47:00', 134.85, 'Card', 11, 3, 71, 0, NULL, 'In Store', NULL, 2, 149.83, 14.98, 0.00, 13), +(70, '2026-02-03 20:24:00', 74.88, 'Cash', 4, 1, 72, 0, NULL, 'In Store', NULL, 1, 74.88, 0.00, 0.00, 7), +(71, '2026-02-04 21:01:00', 27.77, 'Card', 9, 2, 73, 0, NULL, 'In Store', NULL, 3, 32.77, 5.00, 0.00, 2), +(72, '2026-02-04 21:38:00', 105.22, 'Cash', 14, 3, 74, 0, NULL, 'In Store', NULL, 1, 105.22, 0.00, 0.00, 10), +(73, '2026-02-05 22:15:00', 72.79, 'Card', 3, 1, 75, 0, NULL, 'In Store', NULL, 7, 82.72, 9.93, 0.00, 7), +(74, '2026-02-05 22:52:00', 133.81, 'Card', 8, 2, 76, 0, NULL, 'In Store', NULL, 1, 133.81, 0.00, 0.00, 13), +(75, '2026-02-06 23:29:00', 130.23, 'Cash', 13, 3, 77, 0, NULL, 'In Store', NULL, 4, 153.21, 22.98, 0.00, 13), +(76, '2026-02-07 00:06:00', 25.27, 'Card', 6, 1, 78, 0, NULL, 'In Store', NULL, 1, 25.27, 0.00, 0.00, 2), +(77, '2026-02-08 00:43:00', 58.96, 'Cash', 7, 2, 79, 0, NULL, 'In Store', NULL, 1, 58.96, 0.00, 0.00, 5), +(78, '2026-02-08 01:20:00', 127.37, 'Card', 12, 3, 80, 0, NULL, 'In Store', NULL, 6, 141.52, 14.15, 0.00, 12), +(79, '2026-02-09 01:57:00', 118.84, 'Card', 5, 1, 81, 0, NULL, 'In Store', NULL, 1, 118.84, 0.00, 0.00, 11), +(80, '2026-02-09 02:34:00', 176.85, 'Cash', 10, 2, 82, 0, NULL, 'In Store', NULL, 2, 196.50, 19.65, 0.00, 17), +(81, '2026-02-10 03:11:00', 367.69, 'Card', 11, 3, 83, 0, NULL, 'In Store', NULL, 1, 367.69, 0.00, 0.00, 36), +(82, '2026-02-10 03:48:00', 52.62, 'Cash', 4, 1, 84, 0, NULL, 'In Store', NULL, 3, 57.62, 5.00, 0.00, 5), +(83, '2026-02-11 04:25:00', 93.37, 'Card', 9, 2, 85, 0, NULL, 'In Store', NULL, 1, 93.37, 0.00, 0.00, 9), +(84, '2026-02-11 05:02:00', 261.58, 'Card', 14, 3, 86, 0, NULL, 'In Store', NULL, 7, 297.25, 35.67, 0.00, 26), +(85, '2026-02-12 05:39:00', 35.78, 'Cash', 3, 1, 87, 0, NULL, 'In Store', NULL, 1, 35.78, 0.00, 0.00, 3), +(86, '2026-02-12 06:16:00', 116.44, 'Card', 8, 2, 88, 0, NULL, 'In Store', NULL, 4, 136.99, 20.55, 0.00, 11), +(87, '2026-02-13 06:53:00', 305.52, 'Cash', 13, 3, 89, 0, NULL, 'In Store', NULL, 1, 305.52, 0.00, 0.00, 30), +(88, '2026-02-13 07:30:00', 165.99, 'Card', 6, 1, 90, 0, NULL, 'In Store', NULL, 1, 165.99, 0.00, 0.00, 16), +(89, '2026-02-14 08:07:00', 93.36, 'Card', 7, 2, 91, 0, NULL, 'In Store', NULL, 6, 103.73, 10.37, 0.00, 9), +(90, '2026-02-14 08:44:00', 198.88, 'Cash', 12, 3, 92, 0, NULL, 'In Store', NULL, 1, 198.88, 0.00, 0.00, 19), +(91, '2026-02-15 09:21:00', 25.28, 'Card', 5, 1, 93, 0, NULL, 'In Store', NULL, 2, 25.28, 0.00, 0.00, 2), +(92, '2026-02-15 09:58:00', 58.51, 'Cash', 10, 2, 94, 0, NULL, 'In Store', NULL, 1, 58.51, 0.00, 0.00, 5), +(93, '2026-02-16 10:35:00', 309.15, 'Card', 11, 3, 95, 0, NULL, 'In Store', NULL, 3, 314.15, 5.00, 0.00, 30), +(94, '2026-02-16 11:12:00', 68.47, 'Card', 4, 1, 96, 0, NULL, 'In Store', NULL, 1, 68.47, 0.00, 0.00, 6), +(95, '2026-02-17 11:49:00', 49.61, 'Cash', 9, 2, 97, 0, NULL, 'In Store', NULL, 7, 49.61, 0.00, 0.00, 4), +(96, '2026-02-13 10:11:00', 34.80, 'Card', 3, 1, 3, 1, 1, 'In Store', NULL, 1, 34.80, 0.00, 0.00, 0), +(97, '2026-02-15 10:22:00', 88.32, 'Card', 4, 1, 4, 1, 2, 'In Store', NULL, 1, 88.32, 0.00, 0.00, 0), +(98, '2026-02-17 10:33:00', 49.05, 'Card', 5, 1, 5, 1, 3, 'In Store', NULL, 1, 49.05, 0.00, 0.00, 0), +(99, '2026-02-19 10:44:00', 15.25, 'Card', 6, 1, 6, 1, 4, 'In Store', NULL, 1, 15.25, 0.00, 0.00, 0), +(100, '2026-02-21 10:55:00', 181.42, 'Card', 7, 2, 7, 1, 5, 'In Store', NULL, 1, 181.42, 0.00, 0.00, 0), +(101, '2026-02-23 11:06:00', 130.82, 'Card', 8, 2, 8, 1, 6, 'In Store', NULL, 1, 130.82, 0.00, 0.00, 0), +(102, '2026-02-25 11:17:00', 50.37, 'Card', 9, 2, 9, 1, 7, 'In Store', NULL, 1, 50.37, 0.00, 0.00, 0), +(103, '2026-02-27 11:28:00', 74.13, 'Card', 10, 2, 10, 1, 8, 'In Store', NULL, 1, 74.13, 0.00, 0.00, 0), +(104, '2026-03-01 11:39:00', 68.78, 'Card', 11, 3, 11, 1, 9, 'In Store', NULL, 1, 68.78, 0.00, 0.00, 0), +(105, '2026-03-03 11:50:00', 17.89, 'Card', 12, 3, 12, 1, 10, 'In Store', NULL, 1, 17.89, 0.00, 0.00, 0), +(106, '2026-03-05 12:01:00', 63.09, 'Card', 13, 3, 13, 1, 11, 'In Store', NULL, 1, 63.09, 0.00, 0.00, 0), +(107, '2026-03-07 12:12:00', 149.99, 'Card', 14, 3, 14, 1, 12, 'In Store', NULL, 1, 149.99, 0.00, 0.00, 0), +(108, '2026-01-28 09:15:00', 41.36, 'Card', 3, 1, 15, 1, 13, 'In Store', NULL, 1, 41.36, 0.00, 0.00, 0), +(109, '2026-01-29 09:52:00', 68.47, 'Card', 8, 2, 16, 1, 14, 'In Store', NULL, 1, 68.47, 0.00, 0.00, 0), +(110, '2026-01-31 10:29:00', 14.16, 'Card', 13, 3, 17, 1, 15, 'In Store', NULL, 1, 14.16, 0.00, 0.00, 0); INSERT INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VALUES (1, 1, 1, 1, 25.09), @@ -2259,52 +2259,52 @@ SET imageUrl = REPLACE(imageUrl, 'https://images.petshop.local/stores/', '/store WHERE imageUrl LIKE 'https://images.petshop.local/stores/%'; INSERT IGNORE INTO appointment (appointmentId, serviceId, petId, customerId, storeId, employeeId, appointmentDate, appointmentTime, appointmentStatus) VALUES -(91, 7, 37, 16, 1, 3, '2026-03-08', '09:00:00', 'COMPLETED'), -(92, 8, 38, 17, 2, 8, '2026-03-10', '10:30:00', 'COMPLETED'), -(93, 8, 39, 18, 3, 13, '2026-03-12', '13:00:00', 'MISSED'), -(94, 4, 40, 19, 1, 6, '2026-03-14', '14:30:00', 'COMPLETED'), -(95, 5, 41, 20, 2, 7, '2026-03-16', '09:00:00', 'COMPLETED'), -(96, 8, 42, 21, 3, 12, '2026-03-18', '10:30:00', 'COMPLETED'), -(97, 2, 43, 22, 1, 5, '2026-03-20', '13:00:00', 'CANCELLED'), -(98, 8, 44, 23, 2, 10, '2026-03-22', '14:30:00', 'COMPLETED'), -(99, 8, 45, 24, 3, 11, '2026-03-24', '09:00:00', 'COMPLETED'), -(100, 8, 46, 25, 1, 4, '2026-03-26', '10:30:00', 'MISSED'), -(101, 6, 47, 26, 2, 9, '2026-03-28', '13:00:00', 'COMPLETED'), -(102, 4, 48, 27, 3, 14, '2026-03-30', '14:30:00', 'COMPLETED'), -(103, 7, 49, 28, 1, 3, '2026-04-01', '09:00:00', 'COMPLETED'), -(104, 6, 50, 29, 2, 8, '2026-04-03', '10:30:00', 'COMPLETED'), -(105, 1, 51, 30, 3, 13, '2026-04-05', '13:00:00', 'MISSED'), -(106, 4, 52, 31, 1, 6, '2026-04-07', '14:30:00', 'COMPLETED'), -(107, 1, 53, 32, 2, 7, '2026-04-09', '09:00:00', 'COMPLETED'), -(108, 2, 54, 33, 3, 12, '2026-04-11', '10:30:00', 'CANCELLED'), -(109, 3, 55, 34, 1, 5, '2026-04-13', '13:00:00', 'COMPLETED'), -(110, 4, 56, 35, 2, 10, '2026-04-15', '10:00:00', 'SCHEDULED'), -(111, 5, 57, 36, 3, 11, '2026-04-16', '14:00:00', 'SCHEDULED'); +(91, 7, 37, 16, 1, 3, '2026-03-08', '09:00:00', 'Completed'), +(92, 8, 38, 17, 2, 8, '2026-03-10', '10:30:00', 'Completed'), +(93, 8, 39, 18, 3, 13, '2026-03-12', '13:00:00', 'Missed'), +(94, 4, 40, 19, 1, 6, '2026-03-14', '14:30:00', 'Completed'), +(95, 5, 41, 20, 2, 7, '2026-03-16', '09:00:00', 'Completed'), +(96, 8, 42, 21, 3, 12, '2026-03-18', '10:30:00', 'Completed'), +(97, 2, 43, 22, 1, 5, '2026-03-20', '13:00:00', 'Cancelled'), +(98, 8, 44, 23, 2, 10, '2026-03-22', '14:30:00', 'Completed'), +(99, 8, 45, 24, 3, 11, '2026-03-24', '09:00:00', 'Completed'), +(100, 8, 46, 25, 1, 4, '2026-03-26', '10:30:00', 'Missed'), +(101, 6, 47, 26, 2, 9, '2026-03-28', '13:00:00', 'Completed'), +(102, 4, 48, 27, 3, 14, '2026-03-30', '14:30:00', 'Completed'), +(103, 7, 49, 28, 1, 3, '2026-04-01', '09:00:00', 'Completed'), +(104, 6, 50, 29, 2, 8, '2026-04-03', '10:30:00', 'Completed'), +(105, 1, 51, 30, 3, 13, '2026-04-05', '13:00:00', 'Missed'), +(106, 4, 52, 31, 1, 6, '2026-04-07', '14:30:00', 'Completed'), +(107, 1, 53, 32, 2, 7, '2026-04-09', '09:00:00', 'Completed'), +(108, 2, 54, 33, 3, 12, '2026-04-11', '10:30:00', 'Cancelled'), +(109, 3, 55, 34, 1, 5, '2026-04-13', '13:00:00', 'Completed'), +(110, 4, 56, 35, 2, 10, '2026-04-15', '10:00:00', 'Scheduled'), +(111, 5, 57, 36, 3, 11, '2026-04-16', '14:00:00', 'Scheduled'); INSERT IGNORE INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES -(111, '2026-03-08 09:15:00', 87.50, 'Cash', 3, 1, 3, 0, NULL, 'IN_STORE', NULL, NULL, 87.50, 0.00, 0.00, 8), -(112, '2026-03-09 10:22:00', 145.20, 'Card', 8, 2, 4, 0, NULL, 'IN_STORE', NULL, NULL, 145.20, 0.00, 0.00, 14), -(113, '2026-03-10 11:33:00', 63.75, 'Cash', 13, 3, 5, 0, NULL, 'IN_STORE', NULL, NULL, 63.75, 0.00, 0.00, 6), -(114, '2026-03-11 12:44:00', 210.00, 'Card', 6, 1, 6, 0, NULL, 'ONLINE', NULL, NULL, 210.00, 0.00, 0.00, 21), -(115, '2026-03-12 13:55:00', 38.90, 'Cash', 7, 2, 7, 0, NULL, 'IN_STORE', NULL, NULL, 38.90, 0.00, 0.00, 3), -(116, '2026-03-14 09:10:00', 325.40, 'Card', 12, 3, 8, 0, NULL, 'ONLINE', NULL, NULL, 325.40, 0.00, 0.00, 32), -(117, '2026-03-16 10:25:00', 72.15, 'Cash', 5, 1, 9, 0, NULL, 'IN_STORE', NULL, NULL, 72.15, 0.00, 0.00, 7), -(118, '2026-03-18 11:40:00', 190.80, 'Card', 10, 2, 10, 0, NULL, 'ONLINE', NULL, NULL, 190.80, 0.00, 0.00, 19), -(119, '2026-03-20 12:55:00', 55.30, 'Cash', 11, 3, 11, 0, NULL, 'IN_STORE', NULL, NULL, 55.30, 0.00, 0.00, 5), -(120, '2026-03-22 14:10:00', 412.60, 'Card', 4, 1, 12, 0, NULL, 'ONLINE', NULL, NULL, 412.60, 0.00, 0.00, 41), -(121, '2026-03-24 09:30:00', 98.45, 'Cash', 9, 2, 13, 0, NULL, 'IN_STORE', NULL, NULL, 98.45, 0.00, 0.00, 9), -(122, '2026-03-26 10:45:00', 167.70, 'Card', 14, 3, 14, 0, NULL, 'ONLINE', NULL, NULL, 167.70, 0.00, 0.00, 16), -(123, '2026-03-28 12:00:00', 44.20, 'Cash', 3, 1, 15, 0, NULL, 'IN_STORE', NULL, NULL, 44.20, 0.00, 0.00, 4), -(124, '2026-03-30 13:15:00', 289.55, 'Card', 8, 2, 16, 0, NULL, 'ONLINE', NULL, NULL, 289.55, 0.00, 0.00, 28), -(125, '2026-04-01 09:20:00', 76.80, 'Cash', 13, 3, 17, 0, NULL, 'IN_STORE', NULL, NULL, 76.80, 0.00, 0.00, 7), -(126, '2026-04-03 10:35:00', 234.10, 'Card', 6, 1, 18, 0, NULL, 'ONLINE', NULL, NULL, 234.10, 0.00, 0.00, 23), -(127, '2026-04-05 11:50:00', 52.40, 'Cash', 7, 2, 19, 0, NULL, 'IN_STORE', NULL, NULL, 52.40, 0.00, 0.00, 5), -(128, '2026-04-07 13:05:00', 178.90, 'Card', 12, 3, 20, 0, NULL, 'ONLINE', NULL, NULL, 178.90, 0.00, 0.00, 17), -(129, '2026-04-09 09:15:00', 115.60, 'Cash', 5, 1, 21, 0, NULL, 'IN_STORE', NULL, NULL, 115.60, 0.00, 0.00, 11), -(130, '2026-04-11 10:30:00', 367.25, 'Card', 10, 2, 22, 0, NULL, 'ONLINE', NULL, NULL, 367.25, 0.00, 0.00, 36), -(131, '2026-04-14 11:45:00', 89.70, 'Cash', 11, 3, 23, 0, NULL, 'IN_STORE', NULL, NULL, 89.70, 0.00, 0.00, 8), -(132, '2026-04-15 09:00:00', 145.30, 'Card', 4, 1, 24, 0, NULL, 'ONLINE', NULL, NULL, 145.30, 0.00, 0.00, 14), -(133, '2026-04-16 10:00:00', 78.60, 'Cash', 9, 2, 25, 0, NULL, 'IN_STORE', NULL, NULL, 78.60, 0.00, 0.00, 7); +(111, '2026-03-08 09:15:00', 87.50, 'Cash', 3, 1, 3, 0, NULL, 'In Store', NULL, NULL, 87.50, 0.00, 0.00, 8), +(112, '2026-03-09 10:22:00', 145.20, 'Card', 8, 2, 4, 0, NULL, 'In Store', NULL, NULL, 145.20, 0.00, 0.00, 14), +(113, '2026-03-10 11:33:00', 63.75, 'Cash', 13, 3, 5, 0, NULL, 'In Store', NULL, NULL, 63.75, 0.00, 0.00, 6), +(114, '2026-03-11 12:44:00', 210.00, 'Card', 6, 1, 6, 0, NULL, 'Online', NULL, NULL, 210.00, 0.00, 0.00, 21), +(115, '2026-03-12 13:55:00', 38.90, 'Cash', 7, 2, 7, 0, NULL, 'In Store', NULL, NULL, 38.90, 0.00, 0.00, 3), +(116, '2026-03-14 09:10:00', 325.40, 'Card', 12, 3, 8, 0, NULL, 'Online', NULL, NULL, 325.40, 0.00, 0.00, 32), +(117, '2026-03-16 10:25:00', 72.15, 'Cash', 5, 1, 9, 0, NULL, 'In Store', NULL, NULL, 72.15, 0.00, 0.00, 7), +(118, '2026-03-18 11:40:00', 190.80, 'Card', 10, 2, 10, 0, NULL, 'Online', NULL, NULL, 190.80, 0.00, 0.00, 19), +(119, '2026-03-20 12:55:00', 55.30, 'Cash', 11, 3, 11, 0, NULL, 'In Store', NULL, NULL, 55.30, 0.00, 0.00, 5), +(120, '2026-03-22 14:10:00', 412.60, 'Card', 4, 1, 12, 0, NULL, 'Online', NULL, NULL, 412.60, 0.00, 0.00, 41), +(121, '2026-03-24 09:30:00', 98.45, 'Cash', 9, 2, 13, 0, NULL, 'In Store', NULL, NULL, 98.45, 0.00, 0.00, 9), +(122, '2026-03-26 10:45:00', 167.70, 'Card', 14, 3, 14, 0, NULL, 'Online', NULL, NULL, 167.70, 0.00, 0.00, 16), +(123, '2026-03-28 12:00:00', 44.20, 'Cash', 3, 1, 15, 0, NULL, 'In Store', NULL, NULL, 44.20, 0.00, 0.00, 4), +(124, '2026-03-30 13:15:00', 289.55, 'Card', 8, 2, 16, 0, NULL, 'Online', NULL, NULL, 289.55, 0.00, 0.00, 28), +(125, '2026-04-01 09:20:00', 76.80, 'Cash', 13, 3, 17, 0, NULL, 'In Store', NULL, NULL, 76.80, 0.00, 0.00, 7), +(126, '2026-04-03 10:35:00', 234.10, 'Card', 6, 1, 18, 0, NULL, 'Online', NULL, NULL, 234.10, 0.00, 0.00, 23), +(127, '2026-04-05 11:50:00', 52.40, 'Cash', 7, 2, 19, 0, NULL, 'In Store', NULL, NULL, 52.40, 0.00, 0.00, 5), +(128, '2026-04-07 13:05:00', 178.90, 'Card', 12, 3, 20, 0, NULL, 'Online', NULL, NULL, 178.90, 0.00, 0.00, 17), +(129, '2026-04-09 09:15:00', 115.60, 'Cash', 5, 1, 21, 0, NULL, 'In Store', NULL, NULL, 115.60, 0.00, 0.00, 11), +(130, '2026-04-11 10:30:00', 367.25, 'Card', 10, 2, 22, 0, NULL, 'Online', NULL, NULL, 367.25, 0.00, 0.00, 36), +(131, '2026-04-14 11:45:00', 89.70, 'Cash', 11, 3, 23, 0, NULL, 'In Store', NULL, NULL, 89.70, 0.00, 0.00, 8), +(132, '2026-04-15 09:00:00', 145.30, 'Card', 4, 1, 24, 0, NULL, 'Online', NULL, NULL, 145.30, 0.00, 0.00, 14), +(133, '2026-04-16 10:00:00', 78.60, 'Cash', 9, 2, 25, 0, NULL, 'In Store', NULL, NULL, 78.60, 0.00, 0.00, 7); INSERT IGNORE INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VALUES (226, 111, 5, 2, 25.50), @@ -2452,30 +2452,30 @@ INSERT INTO activityLog (userId, storeId, usernameSnapshot, fullNameSnapshot, ro (11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Created a new appointment | POST /api/v1/appointments → 201', '2026-04-19 11:00:22'); INSERT IGNORE INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES -(134, '2026-04-10 09:15:00', 57.67, 'Cash', 4, 1, 16, 0, NULL, 'IN_STORE', NULL, NULL, 57.67, 0.00, 0.00, 5), -(135, '2026-04-10 11:30:00', 143.55, 'Card', 8, 2, 17, 0, NULL, 'ONLINE', NULL, NULL, 143.55, 0.00, 0.00, 14), -(136, '2026-04-10 14:45:00', 50.09, 'Cash', 12, 3, 18, 0, NULL, 'IN_STORE', NULL, NULL, 50.09, 0.00, 0.00, 5), -(137, '2026-04-11 10:00:00', 114.48, 'Card', 5, 1, 19, 0, NULL, 'ONLINE', NULL, NULL, 114.48, 0.00, 0.00, 11), -(138, '2026-04-11 13:20:00', 93.55, 'Cash', 9, 2, 20, 0, NULL, 'IN_STORE', NULL, NULL, 93.55, 0.00, 0.00, 9), -(139, '2026-04-12 09:45:00', 100.71, 'Card', 13, 3, 21, 0, NULL, 'ONLINE', NULL, NULL, 100.71, 0.00, 0.00, 10), -(140, '2026-04-12 11:00:00', 51.07, 'Cash', 6, 1, 22, 0, NULL, 'IN_STORE', NULL, NULL, 51.07, 0.00, 0.00, 5), -(141, '2026-04-12 15:30:00', 139.66, 'Card', 7, 2, 23, 0, NULL, 'ONLINE', NULL, NULL, 139.66, 0.00, 0.00, 13), -(142, '2026-04-13 09:00:00', 73.98, 'Cash', 14, 3, 24, 0, NULL, 'IN_STORE', NULL, NULL, 73.98, 0.00, 0.00, 7), -(143, '2026-04-13 12:15:00', 134.76, 'Card', 4, 1, 25, 0, NULL, 'ONLINE', NULL, NULL, 134.76, 0.00, 0.00, 13), -(144, '2026-04-14 10:30:00', 80.40, 'Cash', 10, 2, 26, 0, NULL, 'IN_STORE', NULL, NULL, 80.40, 0.00, 0.00, 8), -(145, '2026-04-14 14:00:00', 125.90, 'Card', 11, 3, 27, 0, NULL, 'ONLINE', NULL, NULL, 125.90, 0.00, 0.00, 12), -(146, '2026-04-15 10:45:00', 80.62, 'Cash', 5, 1, 28, 0, NULL, 'IN_STORE', NULL, NULL, 80.62, 0.00, 0.00, 8), -(147, '2026-04-15 13:00:00', 141.28, 'Card', 8, 2, 29, 0, NULL, 'ONLINE', NULL, NULL, 141.28, 0.00, 0.00, 14), -(148, '2026-04-16 09:30:00', 97.85, 'Cash', 12, 3, 30, 0, NULL, 'IN_STORE', NULL, NULL, 97.85, 0.00, 0.00, 9), -(149, '2026-04-16 11:45:00', 89.36, 'Card', 6, 1, 31, 0, NULL, 'ONLINE', NULL, NULL, 89.36, 0.00, 0.00, 8), -(150, '2026-04-17 09:15:00', 112.38, 'Cash', 13, 3, 32, 0, NULL, 'IN_STORE', NULL, NULL, 112.38, 0.00, 0.00, 11), -(151, '2026-04-17 11:30:00', 67.49, 'Card', 5, 1, 33, 0, NULL, 'ONLINE', NULL, NULL, 67.49, 0.00, 0.00, 6), -(152, '2026-04-17 14:45:00', 158.20, 'Cash', 9, 2, 34, 0, NULL, 'IN_STORE', NULL, NULL, 158.20, 0.00, 0.00, 15), -(153, '2026-04-18 09:30:00', 84.76, 'Card', 14, 3, 35, 0, NULL, 'ONLINE', NULL, NULL, 84.76, 0.00, 0.00, 8), -(154, '2026-04-18 12:00:00', 203.15, 'Cash', 4, 1, 36, 0, NULL, 'IN_STORE', NULL, NULL, 203.15, 0.00, 0.00, 20), -(155, '2026-04-18 15:15:00', 45.93, 'Card', 7, 2, 37, 0, NULL, 'ONLINE', NULL, NULL, 45.93, 0.00, 0.00, 4), -(156, '2026-04-19 10:00:00', 129.84, 'Cash', 11, 3, 38, 0, NULL, 'IN_STORE', NULL, NULL, 129.84, 0.00, 0.00, 12), -(157, '2026-04-19 13:30:00', 76.50, 'Card', 6, 1, 39, 0, NULL, 'ONLINE', NULL, NULL, 76.50, 0.00, 0.00, 7); +(134, '2026-04-10 09:15:00', 57.67, 'Cash', 4, 1, 16, 0, NULL, 'In Store', NULL, NULL, 57.67, 0.00, 0.00, 5), +(135, '2026-04-10 11:30:00', 143.55, 'Card', 8, 2, 17, 0, NULL, 'Online', NULL, NULL, 143.55, 0.00, 0.00, 14), +(136, '2026-04-10 14:45:00', 50.09, 'Cash', 12, 3, 18, 0, NULL, 'In Store', NULL, NULL, 50.09, 0.00, 0.00, 5), +(137, '2026-04-11 10:00:00', 114.48, 'Card', 5, 1, 19, 0, NULL, 'Online', NULL, NULL, 114.48, 0.00, 0.00, 11), +(138, '2026-04-11 13:20:00', 93.55, 'Cash', 9, 2, 20, 0, NULL, 'In Store', NULL, NULL, 93.55, 0.00, 0.00, 9), +(139, '2026-04-12 09:45:00', 100.71, 'Card', 13, 3, 21, 0, NULL, 'Online', NULL, NULL, 100.71, 0.00, 0.00, 10), +(140, '2026-04-12 11:00:00', 51.07, 'Cash', 6, 1, 22, 0, NULL, 'In Store', NULL, NULL, 51.07, 0.00, 0.00, 5), +(141, '2026-04-12 15:30:00', 139.66, 'Card', 7, 2, 23, 0, NULL, 'Online', NULL, NULL, 139.66, 0.00, 0.00, 13), +(142, '2026-04-13 09:00:00', 73.98, 'Cash', 14, 3, 24, 0, NULL, 'In Store', NULL, NULL, 73.98, 0.00, 0.00, 7), +(143, '2026-04-13 12:15:00', 134.76, 'Card', 4, 1, 25, 0, NULL, 'Online', NULL, NULL, 134.76, 0.00, 0.00, 13), +(144, '2026-04-14 10:30:00', 80.40, 'Cash', 10, 2, 26, 0, NULL, 'In Store', NULL, NULL, 80.40, 0.00, 0.00, 8), +(145, '2026-04-14 14:00:00', 125.90, 'Card', 11, 3, 27, 0, NULL, 'Online', NULL, NULL, 125.90, 0.00, 0.00, 12), +(146, '2026-04-15 10:45:00', 80.62, 'Cash', 5, 1, 28, 0, NULL, 'In Store', NULL, NULL, 80.62, 0.00, 0.00, 8), +(147, '2026-04-15 13:00:00', 141.28, 'Card', 8, 2, 29, 0, NULL, 'Online', NULL, NULL, 141.28, 0.00, 0.00, 14), +(148, '2026-04-16 09:30:00', 97.85, 'Cash', 12, 3, 30, 0, NULL, 'In Store', NULL, NULL, 97.85, 0.00, 0.00, 9), +(149, '2026-04-16 11:45:00', 89.36, 'Card', 6, 1, 31, 0, NULL, 'Online', NULL, NULL, 89.36, 0.00, 0.00, 8), +(150, '2026-04-17 09:15:00', 112.38, 'Cash', 13, 3, 32, 0, NULL, 'In Store', NULL, NULL, 112.38, 0.00, 0.00, 11), +(151, '2026-04-17 11:30:00', 67.49, 'Card', 5, 1, 33, 0, NULL, 'Online', NULL, NULL, 67.49, 0.00, 0.00, 6), +(152, '2026-04-17 14:45:00', 158.20, 'Cash', 9, 2, 34, 0, NULL, 'In Store', NULL, NULL, 158.20, 0.00, 0.00, 15), +(153, '2026-04-18 09:30:00', 84.76, 'Card', 14, 3, 35, 0, NULL, 'Online', NULL, NULL, 84.76, 0.00, 0.00, 8), +(154, '2026-04-18 12:00:00', 203.15, 'Cash', 4, 1, 36, 0, NULL, 'In Store', NULL, NULL, 203.15, 0.00, 0.00, 20), +(155, '2026-04-18 15:15:00', 45.93, 'Card', 7, 2, 37, 0, NULL, 'Online', NULL, NULL, 45.93, 0.00, 0.00, 4), +(156, '2026-04-19 10:00:00', 129.84, 'Cash', 11, 3, 38, 0, NULL, 'In Store', NULL, NULL, 129.84, 0.00, 0.00, 12), +(157, '2026-04-19 13:30:00', 76.50, 'Card', 6, 1, 39, 0, NULL, 'Online', NULL, NULL, 76.50, 0.00, 0.00, 7); INSERT IGNORE INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VALUES (264, 134, 1, 2, 25.09), -- 2.49.1 From 251c785762b324e7ce131bcc8c1843eb78966f4e Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 09:48:53 -0600 Subject: [PATCH 30/34] fix remaining staff roles --- backend/src/main/resources/db/migration/V2__seed_data.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql index 0a307195..3134a995 100644 --- a/backend/src/main/resources/db/migration/V2__seed_data.sql +++ b/backend/src/main/resources/db/migration/V2__seed_data.sql @@ -58,7 +58,7 @@ INSERT INTO storeLocation (storeId, storeName, address, phone, email, imageUrl) (3, 'West Side Store', '789 West Blvd, Calgary, AB', '403-555-0103', 'westside@petshop.com', 'https://images.petshop.local/stores/west.webp'); INSERT INTO users (id, username, password, email, firstName, lastName, fullName, phone, avatarUrl, role, staffRole, primaryStoreId, loyaltyPoints, active, tokenVersion) VALUES -(1, 'admin', '$2y$10$ok/BmOn/pyyamTeNmUDiB.OfLCduQlZSAaRLlupM/cZb7ZhiBriVe', 'admin@petshop.com', 'Admin', 'User', 'Admin User', '000-000-1000', 'https://images.petshop.local/users/001.webp', 'ADMIN', 'ADMINISTRATOR', 1, 0, 1, 0), +(1, 'admin', '$2y$10$ok/BmOn/pyyamTeNmUDiB.OfLCduQlZSAaRLlupM/cZb7ZhiBriVe', 'admin@petshop.com', 'Admin', 'User', 'Admin User', '000-000-1000', 'https://images.petshop.local/users/001.webp', 'ADMIN', 'Administrator', 1, 0, 1, 0), (2, 'morgan.lee', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'morgan.lee@petshop.com', 'Morgan', 'Lee', 'Morgan Lee', '403-700-0002', 'https://images.petshop.local/users/002.webp', 'ADMIN', 'Operations Admin', 2, 0, 1, 0), (3, 'staff', '$2y$10$23mqbLolo609T/.PC4KfiuY.9HqYEgA8LrJ/fccZ7CmK0/OIsPrfq', 'staff@petshop.com', 'Staff', 'User', 'Staff User', '000-000-1001', 'https://images.petshop.local/users/003.webp', 'STAFF', 'Store Manager', 1, 0, 1, 0), (4, 'sara.smith', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'sara.smith@petshop.com', 'Sara', 'Smith', 'Sara Smith', '403-710-0004', 'https://images.petshop.local/users/004.webp', 'STAFF', 'Sales Associate', 1, 0, 1, 0), @@ -1763,7 +1763,7 @@ SELECT 'ai.bot', '000-000-0000', 'https://images.petshop.local/users/bot.webp', 'STAFF', - 'CUSTOMER_SERVICE', + 'Customer Service', NULL, 0, 1, -- 2.49.1 From d627272d485c67510dbf9ec5db6a04dc152fb6cc Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 10:10:49 -0600 Subject: [PATCH 31/34] evict cache on logout --- .../main/java/com/petshop/backend/controller/AuthController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index 032518ec..e1d14e67 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -347,6 +347,7 @@ public class AuthController { User user = authHelper.getAuthenticatedUser(); user.setTokenVersion(user.getTokenVersion() + 1); userRepository.save(user); + userAuthCacheService.evict(user.getId()); Map response = new HashMap<>(); response.put("message", "Logged out successfully"); return ResponseEntity.ok(response); -- 2.49.1 From 832d1f2c339c7d2343fc1636cbc25a94a767fcc3 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 10:26:01 -0600 Subject: [PATCH 32/34] validate pet price and species --- .../java/com/petshop/backend/dto/chat/MessageRequest.java | 2 +- .../main/java/com/petshop/backend/dto/pet/PetRequest.java | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java b/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java index 37dcd683..0784054b 100644 --- a/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java @@ -1,7 +1,7 @@ package com.petshop.backend.dto.chat; public class MessageRequest { - @jakarta.validation.constraints.Size(max = 10000, message = "Message content must not exceed 10000 characters") + @jakarta.validation.constraints.Size(max = 2000, message = "Message content must not exceed 2000 characters") private String content; private String attachmentUrl; private String attachmentName; diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java index ff4f9ad2..c22a5da6 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java @@ -1,8 +1,11 @@ package com.petshop.backend.dto.pet; +import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; import java.math.BigDecimal; import java.util.Objects; @@ -11,6 +14,7 @@ public class PetRequest { private String petName; @NotBlank(message = "Species is required") + @Pattern(regexp = "^(Dog|Cat|Bird|Fish|Rabbit|Hamster|Guinea Pig|Reptile|Other)$", flags = Pattern.Flag.CASE_INSENSITIVE, message = "Species must be Dog, Cat, Bird, Fish, Rabbit, Hamster, Guinea Pig, Reptile, or Other") private String petSpecies; private String petBreed; @@ -21,6 +25,8 @@ public class PetRequest { @NotNull(message = "Status is required") private String petStatus; + @PositiveOrZero(message = "Price must be zero or positive") + @DecimalMax(value = "99999.99", message = "Price must not exceed 99999.99") private BigDecimal petPrice; private Long customerId; -- 2.49.1 From b380681be324d3cbcd992010d73c33e6b08c8a4d Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 10:41:49 -0600 Subject: [PATCH 33/34] auto-refresh product list --- .../controllers/ProductController.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java index 07b94d0a..31f4164c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java @@ -1,5 +1,7 @@ package org.example.petshopdesktop.controllers; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -8,6 +10,7 @@ import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.geometry.Pos; import javafx.scene.Scene; +import javafx.util.Duration; import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.image.ImageView; @@ -78,8 +81,9 @@ public class ProductController { private TextField txtSearch; //data declaration - private ObservableList data = FXCollections.observableArrayList(); //empty + private ObservableList data = FXCollections.observableArrayList(); private String mode = null; + private Timeline refreshTimer; /** * Set up the table view for products and display it when starting up @@ -130,6 +134,15 @@ public class ProductController { } }); + refreshTimer = new Timeline(new KeyFrame(Duration.seconds(30), e -> applyFilters())); + refreshTimer.setCycleCount(Timeline.INDEFINITE); + refreshTimer.play(); + tvProducts.sceneProperty().addListener((obs, oldScene, newScene) -> { + if (newScene == null && refreshTimer != null) { + refreshTimer.stop(); + } + }); + } /** -- 2.49.1 From a10841dd027e237edd627e25794fd4bcaed07a76 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 20 Apr 2026 10:45:45 -0600 Subject: [PATCH 34/34] add XSS content filter to DTOs --- .../backend/dto/category/CategoryRequest.java | 2 ++ .../petshop/backend/dto/pet/PetRequest.java | 2 ++ .../backend/dto/product/ProductRequest.java | 3 +++ .../backend/dto/service/ServiceRequest.java | 3 +++ .../backend/dto/store/StoreRequest.java | 2 ++ .../backend/dto/supplier/SupplierRequest.java | 1 + .../petshop/backend/dto/user/UserRequest.java | 3 +++ .../com/petshop/backend/util/SafeContent.java | 17 +++++++++++++++++ .../backend/util/SafeContentValidator.java | 18 ++++++++++++++++++ 9 files changed, 51 insertions(+) create mode 100644 backend/src/main/java/com/petshop/backend/util/SafeContent.java create mode 100644 backend/src/main/java/com/petshop/backend/util/SafeContentValidator.java diff --git a/backend/src/main/java/com/petshop/backend/dto/category/CategoryRequest.java b/backend/src/main/java/com/petshop/backend/dto/category/CategoryRequest.java index c012ae21..b099826f 100644 --- a/backend/src/main/java/com/petshop/backend/dto/category/CategoryRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/category/CategoryRequest.java @@ -1,10 +1,12 @@ package com.petshop.backend.dto.category; +import com.petshop.backend.util.SafeContent; import jakarta.validation.constraints.NotBlank; import java.util.Objects; public class CategoryRequest { @NotBlank(message = "Category name is required") + @SafeContent private String categoryName; private String categoryType; diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java index c22a5da6..4d0986bb 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java @@ -1,5 +1,6 @@ package com.petshop.backend.dto.pet; +import com.petshop.backend.util.SafeContent; import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -11,6 +12,7 @@ import java.util.Objects; public class PetRequest { @NotBlank(message = "Pet name is required") + @SafeContent private String petName; @NotBlank(message = "Species is required") diff --git a/backend/src/main/java/com/petshop/backend/dto/product/ProductRequest.java b/backend/src/main/java/com/petshop/backend/dto/product/ProductRequest.java index 71dd600f..e1ca9241 100644 --- a/backend/src/main/java/com/petshop/backend/dto/product/ProductRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/product/ProductRequest.java @@ -1,5 +1,6 @@ package com.petshop.backend.dto.product; +import com.petshop.backend.util.SafeContent; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; @@ -8,11 +9,13 @@ import java.util.Objects; public class ProductRequest { @NotBlank(message = "Product name is required") + @SafeContent private String prodName; @NotNull(message = "Category ID is required") private Long categoryId; + @SafeContent private String prodDesc; @NotNull(message = "Product price is required") diff --git a/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java b/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java index 977b72cc..464fefda 100644 --- a/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java @@ -1,5 +1,6 @@ package com.petshop.backend.dto.service; +import com.petshop.backend.util.SafeContent; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; @@ -10,8 +11,10 @@ import java.util.Set; public class ServiceRequest { @NotBlank(message = "Service name is required") + @SafeContent private String serviceName; + @SafeContent private String serviceDesc; @NotNull(message = "Service price is required") diff --git a/backend/src/main/java/com/petshop/backend/dto/store/StoreRequest.java b/backend/src/main/java/com/petshop/backend/dto/store/StoreRequest.java index 5bb68613..fca03db1 100644 --- a/backend/src/main/java/com/petshop/backend/dto/store/StoreRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/store/StoreRequest.java @@ -1,11 +1,13 @@ package com.petshop.backend.dto.store; +import com.petshop.backend.util.SafeContent; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import java.util.Objects; public class StoreRequest { @NotBlank(message = "Store name is required") + @SafeContent private String storeName; @NotBlank(message = "Address is required") diff --git a/backend/src/main/java/com/petshop/backend/dto/supplier/SupplierRequest.java b/backend/src/main/java/com/petshop/backend/dto/supplier/SupplierRequest.java index b7ae7efb..a0eb2b6a 100644 --- a/backend/src/main/java/com/petshop/backend/dto/supplier/SupplierRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/supplier/SupplierRequest.java @@ -1,5 +1,6 @@ package com.petshop.backend.dto.supplier; +import com.petshop.backend.util.SafeContent; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import java.util.Objects; diff --git a/backend/src/main/java/com/petshop/backend/dto/user/UserRequest.java b/backend/src/main/java/com/petshop/backend/dto/user/UserRequest.java index 774e9d90..04c82e43 100644 --- a/backend/src/main/java/com/petshop/backend/dto/user/UserRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/user/UserRequest.java @@ -1,5 +1,6 @@ package com.petshop.backend.dto.user; +import com.petshop.backend.util.SafeContent; import com.petshop.backend.entity.User; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; @@ -15,10 +16,12 @@ public class UserRequest { private String password; @NotBlank(message = "First name is required") + @SafeContent @Size(max = 50) private String firstName; @NotBlank(message = "Last name is required") + @SafeContent @Size(max = 50) private String lastName; diff --git a/backend/src/main/java/com/petshop/backend/util/SafeContent.java b/backend/src/main/java/com/petshop/backend/util/SafeContent.java new file mode 100644 index 00000000..6db91b6e --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/util/SafeContent.java @@ -0,0 +1,17 @@ +package com.petshop.backend.util; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = SafeContentValidator.class) +public @interface SafeContent { + String message() default "Content contains prohibited characters or scripts"; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/backend/src/main/java/com/petshop/backend/util/SafeContentValidator.java b/backend/src/main/java/com/petshop/backend/util/SafeContentValidator.java new file mode 100644 index 00000000..6d3af630 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/util/SafeContentValidator.java @@ -0,0 +1,18 @@ +package com.petshop.backend.util; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +public class SafeContentValidator implements ConstraintValidator { + + private static final Pattern DANGEROUS = Pattern.compile( + "]+on\\w+", + Pattern.CASE_INSENSITIVE); + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null || value.isBlank()) return true; + return !DANGEROUS.matcher(value).find(); + } +} -- 2.49.1