From e6c45971eb6065bdff47f897c3e402913e67c099 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 28 Jan 2026 02:13:53 +0000 Subject: [PATCH] Autosave: 20260128-021353 --- core/__pycache__/admin.cpython-311.pyc | Bin 15106 -> 16409 bytes core/__pycache__/mail.cpython-311.pyc | Bin 3960 -> 4430 bytes core/__pycache__/models.cpython-311.pyc | Bin 24726 -> 27204 bytes .../__pycache__/notifications.cpython-311.pyc | Bin 0 -> 10338 bytes core/__pycache__/views.cpython-311.pyc | Bin 55391 -> 55612 bytes .../whatsapp_utils.cpython-311.pyc | Bin 13717 -> 12093 bytes core/admin.py | 30 ++- core/mail.py | 41 ++-- .../init_notifications.cpython-311.pyc | Bin 0 -> 2420 bytes .../management/commands/init_notifications.py | 30 +++ core/migrations/0022_notificationtemplate.py | 32 +++ .../0022_notificationtemplate.cpython-311.pyc | Bin 0 -> 2822 bytes core/models.py | 39 +++- core/notifications.py | 183 +++++++++++++++ core/views.py | 41 ++-- core/whatsapp_utils.py | 218 ++++++++---------- 16 files changed, 457 insertions(+), 157 deletions(-) create mode 100644 core/__pycache__/notifications.cpython-311.pyc create mode 100644 core/management/commands/__pycache__/init_notifications.cpython-311.pyc create mode 100644 core/management/commands/init_notifications.py create mode 100644 core/migrations/0022_notificationtemplate.py create mode 100644 core/migrations/__pycache__/0022_notificationtemplate.cpython-311.pyc create mode 100644 core/notifications.py diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 13a066665f2a206e1102b6067c78a96d9cc7d66b..870921fe83414f0ac4d50f972c0be96f6482ae5e 100644 GIT binary patch delta 4162 zcmbtXeQXrh5#PP<51-E;cW0YBpVuG69N5OV20{%6#8>!+U?91wT-UeH_QBqr^L7to zm-L*(O^~9rLQhIc6{YY;p_E2Jz4VVDgqjK}rD|1!EY;msZU1Qhh^UaOR8^@e(V4ft zvvYPzr7phN+272YH*em|yvM6&FHMS-*L*&Y0MD(jji)ZGxmZ~*KDyX`IG^^%{i2`< zibJhRSI28O?^J8kfp{Ptj0gF*OAV##;&q&NtM%!IcmwA>s*qk0Uje*L@v7l;Bp%^- zh1!^o#-kkfsZD7qE^)k4ZH_m?T$O4|yanEVwV^fL7H>L zVRyv3HSHX$0j!o-uUZA+2K>w?zKY8NAPa(i4E6o1ITiw}u86S@M2EW`kcN4rzLG1g z04Q8kvUdX4#co=f*kdu^?J7=kxtm>Vt!)*EovgZQf$8CAKB9mlieDu?7g_y3>A6YA z)AiV|ETM8=m}p&sfqxZb&C~6Ho{w}sU5{O{)|x?zK#&@XLGrI#y5z7x{A69ZNu~0b z5R0-~)|lvKf3}_s8G%DtJvEd{%6clB86fFVRn|$N&-Pdpqint77mf&ol17-_`KDt7 zXA5sT@V*A)|~*vEOZy*5|TYYG~6LcqFj3m+wIv3 z+OK;q2f4P*u&F6cH|$z&lu){f^?4(m&%p@8Ny$q&qUm%mez_Bgq$+D#BB9y1J!#Uk zc!W)Phs6zrzk0o5)nQb14HG?)qpC)$nbX(P!L8X4Fuci}rf1Wp^m$M&Gj@Uz-)ms( zQ{MxtxQpGc{;}B1vNiqrAR4xVgIP*@fTt^v)FA2{ku_P@sk!WC4von)lgbPmjx^Cn zvI;$gs1^L8AuiW)$t2MsYAw83drO44?+)IBbyb8;^(N5(-zjvN z29WUeSVqs7OI)&f+rZLW&tNhbu9G9Ou5BM3rF<7_XeW-x>Z5CsMA*@~RgTTb_ON_i zU33euhI25RRf(LTM^JDA$%{y~u@CE-Lwr9(_}*qZz6nK}*u%PinhCgxeOiC7<`qO= zMe-Vun2o+%_-Vrpi`c-_NI3X13ci8l%SiG_CfU1@-UFy@cygl(q`8ui^_Zg+U04oe z8Xeb&Zn!AXMza|R@wag-=APyNVusXo?2SUCu?nNv8Ep!dEnLS@IVMLVZEqmUljSUO z>ycpaOe5!!JI}5}Uocm9zVIZvQ9UANFE z`$O~1=yl7X-7!1uMs;U)@H9#4n&BYlprkZIR2I2hw(pIc}4WTg{qkH zw3*56iUSjDqLY2ELt+oxtMa5I9zrgJ1nY~ZGaW=uK{A4bPn$xHm&Bq?qdaf11gMH6 z4P?nyeE{G@`w{4ELs^1 z$G+RqQ|@p*&Q@~RaPK;oB%|0xx$lM*TGkgYz`l@mv=0m!A~8L;!v{|?Gd*si<89pW z!v4;WEUg6qV%CK^U8Y7^dpE1W3^SbDljx8(a9yOg*)O}V9ml+)cT5|&vxX15$*Zmj-6D>S(e!ni6=V{G5fxo;H@B-j=Fxkw6!d;=b zLV6p?StRFx7=B7*C7V&l=RKo14;@D`0Ynpl%y)-N2qFP#;ZDz2M6--8v8LYPu1*v# zbR%ArZzEP#gm)3U%RcN~X^y_j{@D9`$2~yd_<>fHAQ{?^I=t6d7V$QUTJ-1atLvlt zP~{L*?Q+xRPz~oce0b0$h?3IfL6wxd@(JpF!tSkKYeo~llkh7&&Ux-LH1>`_gt({( zaf>3xt%@aXgSTC=!nMzm5{E1nfjAWVRfyLOj2V9v2S=S{sOw$JyuRC%VAC`Qc=!PC zas!5}ai>x-CB$7yrQ(|s<8H+YR}8<=u-v_3yOK_2Viv=ChK$o6fN{v=X?StosFlyk zDHVf~I4jds9y#q0#gv`!?Mj18+JWa{MvvJHkCx+Y4z^~2m#N_+sF)bcD&r_x{3+9k zJ-f41;u>0qq>m5@^`w+0jZt}YlqgavBgxXBtPx4i&tHbRq~q}U+`z#DQff%b5TX#J zi(W&os*1;7$uWyjHHJqbSS$HZhJYJu(2e5Q4hc?j3GQ7mHy--ZSV|v(d7NWvDnqm` z_Q)N|B?D~W z9?kz2ESo1MZ!w-_>+EN!#B02)I+-%yTdc?@N8&cHXQlRy6MKD`{Ci4 z@bH}78U;<0fVzvTPkS5hyQ4Gi=xnHeUVq>H@cxeR43}?G?dP>#0bSh^wI1cf+ssb@@{D!xv z!*9(9*Wl2qc~~3CQazc2V*(Fhqq?ZgEsp4E)jXDhD6A}%0eiB+kq;qOG!K{5AzFus z4U3iHNoqLZdNT}{W%{kfsw28SN*e&-H*LdrOqY{qh_W|>wL$s+O*Wyv=U^7HhW8iB zuT+N=_yH6jK0ur1nk7;5n-(itOgI0rsbjZQJtG z!vFPG=zsPj%fOtaPK5niN-o=Hr0&0=;O`649zk?Wd440*J`ifB1FNQ2@3=4QoDp_T J7ytM+{|5vo0xtjn delta 2932 zcmbVNO>7(25#A*!k|sq;qWCMB)ylLi(bkWxNU9>qa@;7fi=v7WS#ipsAy(WcY2{rm zJG+cz*{PHua1Koaqz?g-Lx2bhbqc=OIBMog zdBMkwZ<$@CF2TnQrKI3maihD`jqeuYm{};L3g+?Baar<77bLCqRY_}O@@fxP)9L=w z2|*^1Y-j!t{E~DV|K3yT6}lwqQaGTG2DFxnf=nZs*(Xm5GK*wxpFAbV4kSDG$$k_E zc2{_BkiQ&H@~`BSJizbEN}Bb4;6oSi?>(|a=BVn*gjw-W-4S-`FdhHBFDWMP;vxS7 zDbvhO9%i43vHrt!{HDJxsKw`{;!)laD9T|z6S&;C8~C>@=lSj6Z-braHFWZ8p`Qdt zJ+Yk2HG0D5<=|QRp614p+rcpsCwV?{RvzWgN2UvpBT@9h4DnqAPa@b91VsU1J;gtY zoI~rcBJZVyb-)c6y6w14cD=>`#`)do(UuE1!VLlYQJvWiT;zX>W}{;GQ>fTD&UYe7 z-rhVbk2V&YqjKvknTMB|Q?7$yLo0tNHY{J@cVe03)2Olo2#TU?c`huV!#V!X*eVV$ z$A9OSXZUd9SF*zYl9=91(o{|8hy`#4Wyle<5gS@jZPjtW+nyq*H5ICQb=eJ?%vrHC z_#v@=^aN5uZ`UgoX4`H+(<=@P2i_vr$^l^z>eBwG=Dg`{=@;kvJO#oNu- z&f^@v-(KL~&a9rGb9Td*S5(J7U#o#QoCNgK9F*oTNYKfDlj)nBB$1-yw%IEdIAx95 z70_#rZdGB5_|T$d8BDDL9T+SSJV$VjpUifrpGMJ^5gzkSWSmsvT+RN=%eHY|&V8QP zBKBp1R}hK;xZW7+xa~WW#V^-iv0rq9YoI#}+_3ky*5EpszeMmT!H)?xc_KfOqdDA2 zy{4%S)5@w-3_b`tFXywJZ;;_d1Ura;xq^@9+l|K0@~!eFMQ0=Z#Ci?4tIRWlRd5=` zv6RQ>pPem64#vr*`5Xi{W-}G=z?XDpXtoNS!GD531{ zNxZvj){D)xM!joBR`SRfLvRUSSU}dYS@iGar3B*4sH5)H(Jg+zuY=o# z1>sml<~ylmKRd+Nl`<>>1)hJphM|3j~a)jQQUx~c84jHDC+RWvLSs00;)WdgD6 z3Q3{}ck_V=g+PqCLXv^-wZ!-(k{i8qhUzR?V9tTHq`&|7KCH~fKZ<{Q*ue?@*Ha(n zsgB(^u!>&OS=A|fP0!ICgTWlX+y8<*!CME04{jkt>+fyBJ@UkQg?V*GyK(*49uMDA z(?jPsLFO{^_R@qOl;RI^{6qZQI4-u**csUN749H`r@z-gzM*;WcTe*KC9E4d@1^vm zU&ye{zjNm193?0G+M7cpSB!eIR{;zYj?x~pakuGtl?RW|D3NBn?rnL+@Cko(_`0Y- zuPe12tTs_bV)!kZV}XE<2WVrkO;9ITLvUMwshU+au0C)GJ|WdQ!BvEZ`_kJ)5}=|n z^2qbDSAuWzw?>u+`$@Uq8EyJ}i{L;h?+~}cPmcC@qj&hi=rfz2B8xX1Hpw!p!dbEr zUC#HA900O{&&V_)E)2{YR^69XD9XZq7K`ee{BdYJIpxME-^xtW9d!{iE_J}kdt`Wz zPmT>mM}>$`Jk39zxH0Z#wNnNkY> u8=C)wr%X$$8*CcB#Njgj?J@rP)MFF+7t;UvYc!wT_C>#ty74EP{(k{bG{*D* diff --git a/core/__pycache__/mail.cpython-311.pyc b/core/__pycache__/mail.cpython-311.pyc index a080a1bad601425b60113859c7f12cadcde37578..79d3c6a178f20efccdf37eddf0a8d46b2b5b554d 100644 GIT binary patch delta 1593 zcmaJ>T}&fY6u#5x&$OK>T^0)EXPGS`c6E1&%g@3}4HQ@n0$GZ2*O*LZnV}5nOzZ8` zt~g^|eQA6#IuCB*Pk2HN`(ljAJ}SJJm}~~qWSb@=_~3g@G$AG?o;w8uNjy3Cn>*h* z_uO;OJu?Rv9>+poaa;g_d?~)Iv~k~t5^m}nYtH>Q#aXkSOWrmx;i&bF=Lr?GQr;NV zVZG%w$Gd^9g|NJ=fvk6XH$Bw4o zNz0${sy;J7krV?NM)jLPNHMVH)RM&52wJ~VvDZ!3U~#*_uCat9g3m$r!IO;P+jPMe zFd9N3ZLk;V9Y^m7J%w<;$?2rPhqBQG;ahG?L4*4Yec{@Kf^ETQ2!-w-i8Z-*IgM#N zi&PQaqwk<1W%yI*Oz>y^6Ff|UUzfQ54=#mLhui!cKHjvRupS2zCG3Y>;#93YE$f1o z)s+=xS=5!RCM;()9eO5C8NI66PI;iC5u%-lvG8lxnvE(Fd0!x7sRx@ zetupq^2vrDKPTt&K*rtpGQ`3p2);t#Ri_!rZNTDIr_GA1qMjFXIYDhKS|6B!mXjr3 z&+^29clP67BW@G991l70hxw4XI>ygi_t{u!CVsBQojg$iv&oI)k4R)6m%t%$K%8jBhX9CY;HBH$%0l;@5&foBK)(2_Z3B~ zl@u+FpM%$U$SEKmC3ffJP8tKm)15EeU90(>(h6!mUGo-2@^p$*3p98*^x?-z_OJPJ zIJ*J`!GrLY4*~F}1UsK>OUWtb(M28B62AoQBV^qN(1q`zI@9B6tui<4)4Jc;^L5{& zn|AXQXxX2}GKM0fq^)Ta4QC0S;eOjyRxP4mpBYl;~MusYpq4LdYQ>2b4Zi;#^Qt7NHtTCSDZou$kz=Ua-abft#A1gnGU| z5lGVLNAAT*4sEj(%(uC~WP;vq>z*8?w+B6h8TAk*;fDDR9Y_w*J6v~ifZiGKB!{f7 w(ETf6h~%_TqY=qv;Rpfe_fGEhFXja`D-|+w0xwuUhd%D=rCRIgEI2Fv0d10*i~s-t delta 1137 zcmZuxO-vI(6rSz=m8BF?LCXIK2o#VI1xc!sG$I}d94Hq~q;#ir&9;lP+ZJkT)e~3U z7!SroE^49|W4xMh@@6<-Y&hw~tE-WOn3(ux%MuMvcE5e|y>H&UdGoR#dJnsU&l?&P z1modMUK0bagVO>RvFC-~T)>_aho#ftqG9`%_=!{OxYWzF*?Fmar4#7=ASFp{T_ag* zMb}J=b}(%hgKp-QDY=;=z_a+Yok5sEk3BBWwYD-zqe-&rO4`ckD{b8fy)(>J{Z^lc-WW^`E^X z0v(0>p{zUUIR>mHi;%pg6V?6}Xcrb{BeIPf!j%E$o@5YVFw9^CAR^NceD=j!L%N|@ z(ER|b#L7|jwdfasH(5iumBpC4P2KD^_0eID7eqle#q{q1?x2bj4v8(*(8LRMH#7my zmhAZW@K_c1J3M?H1n~LHfFpVnM?z;u9od_11r8L{-n-X!ezz_Dc4Kd9ara_;D|jHG zrk-kZN3|JOJ0jK4pi?I{Dc=#mPX-PW>y*QT0FMGKw$?ZkZh>^}U7AMkl-M}G#m^ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 6e3aca3d4b22482e2521f31f4f9271fe842fe271..e161775e70dfe26c59e313c7cf5c11ebba317ec4 100644 GIT binary patch delta 2302 zcmZ`)&uS9%)7I>uRN1zEtX8U{nr0c4jwr96M~{mznpy?|t*$ zJa4}LjeTRY-9H8beug~H7dDlf7oK$Yv6p^1HOTon=5q!ff5bpg3bKQTjzM3BNesQZ z$~~TEyb z&Y_nai6Niem_ts?;rbpPCV6}?4X6<^z00{)#M>jJ{px(mz8|MN8#&q zN&GzZYgk=UvPfLlAjw)Vc^Wa}I>DEYlZ5u^D~gswc|(*jB5_cp4h`YLKxP*KDHj28 z>LE~q@B~fTbZGjzEF)cS(E4Xw0AyUC&N3ZZe?x9V(UnDVJd9+dY!JP{hTc~$1md@L zF)A2=n1+d(BCoA$4LRYm;|4R1yDsKvMfWkxJ*B=DPP0sSA4zm~g;)#2-HxXuj1{*Z z(MDmqAsOp>z3tI@RVK(dz+UV7C74t4qLfA0*cB2b(oD@L98FRPBq?+r?20EH3_*Iq zvYIy}*$|ghoD-cqg>cICfh)uZ;!cJH{+oRHW)mJ9G|2+rq~81f-D(Unq@=6ibSqf z2hxV5Wa$zpdDG2n>`pm|qp{^N@ZbTERfxdDho|W}FXiYuL6xWo4pSw5GDq@|fm&Qe zvH{|gHxjg>I*!K=3QFEe6nb3ZB^7Q0mC_1gfUShZvj!fMqbpqK*&X=!bkt&hyUb<%Y5AC<5tgzb$Fx% z#_mq`ADCeZ8B!tA41xOVrVwp+m%i&d-g$+{U1v+*r22#p%feY(IQ#fWC3gIoWPwl0 zv2%9pTxsZN#N_cPITEIo;xoilyV8?$V(RMjwbWz=)BhLvBkJ~$Qt=;j`-X0N8?zxc z=B4V_-76MRes3+|Y5>sz5EsckQmA=5OyJ;sdPkC`H6mI(5-wT60ds;>gsB;&ot!47 z*!Zb&f!@wyx)x))1|JXG{(%>ZKaakKmOowgr)___n5yt6wuWu~#I|f*%v-!#=2e?l zi|NY1$g^R4V5G({6B&-!JDf=W3@&hQ9sAC)me&+&oeW75wL?SOnP0tDTC_q7<# xb5Q!{?!W9rseJF4|27FAzl{F-tN-q8apz+nutcZbJe&C8!dvQpcL3;U{SP5juXg|d delta 106 zcmX?dg>l+JM!w~|yj%=GAQE4hxzc1Jp9G`KMs;Z>wiM1_22J_Rc}yKqOq#Nr{nMBk xl?{RFiWEVF9FX|MVUwGmQks)$S7gh`zyL%H#nU!drB7huW8h^p`T!!pssUH?7xw@F diff --git a/core/__pycache__/notifications.cpython-311.pyc b/core/__pycache__/notifications.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93140b86dbdc628a2ca67163359f3296e8e3f476 GIT binary patch literal 10338 zcmd5iYj9IndRLNdS+dQ;!Y^#YB^a=H$eRFxHDK_IgkX#pJ7l4oii^&*Ei6mwePDcJ zZ&;QPLAx!xvpdUXL&K!AU5f{h(k|P{( z#kfh=X_t#5ZsLi#Cq1VGH{n(-rJ@dCYs->*hr7xk?L}=(`PAK7z}5 zo%WLo;w6>DNBpFUUDfYbk{ak+K~|DgWHr06uhxVMkTnD^?5iX7q=7V&CbE_^lNQ$3 zO4gC}geM!w3+zI;jiiliBAdw;(oTY8D}&ocwv!zsL^{ZK*oAN}l9$L%vWx5{FS85# z_K>~gyQGusBm2n#xUjE_93+QGH|Zh0?83f2(oYVPSIDd62)nTFHFA^;kYi+!94ABM zdu;59_p47=kr6U{*>$>_ln-;k*Z+jV2)gv@F;NM}g!q^$jER3jAwlPw!GsbWjYb3| znuw2xlgXH%h(WhrY2EAI-b7pxFDQfBwqY@@@KfVroELai7A0O1$D*<#N;2QxKPg0G z9dC>airk${1~va%Z}E`?5&79!ZS_e}vM1qtp;ryS^mIP^4*h$c-pO93_vnM{rR*hb z>#2k)@y7&NkoYB|_^8b1(5gF~aTo}HOYhK6csiH;F`s=mo5~v(tkkQBC`Y7d5}DS0 z=LIP$oQ;XH?w8fG=fsE-7UQ{lLDFkPmZ2%FJiW^k3!Qf=)>$a`q8trYFej~;D^Vf z$s`yjGgM_umkZ5uej9BXdGl~n7Xr0^9H1h1h>QW>jG6DRAH*~LUiKYe284bO1&lvFg_}8p!QX)& zT%mVMVbFg6su+tU5WPjd-_HbVu?9YA5QDb1WeICr_;Eqz&x+u~l1XjIKVq`V4>DFe zovHT^GuGKtTMB;vP)rnL5w#u>fpt(oQkZ69f!j%80@SbY=c95IJPe-_2}C5xA@yHD z7+`$!^Z}g%Is9cFQ}nO={^ygE1BW3$Smz}sWRUAUc+b%%rFde-{D8H0i#Y?co2U1) zSJ6f8fyMw0XokT)%>IZzK?k`{pJXp*-vzf|tlWk%ccCAipi|ujI<94}gtWU$)%LAw zt)$XI>ceH#@~vydR!U9}A+42#p*-IgMjP;kq%2hA(h$GniK%=i63#jVk-4C-@mNRPxWctPf!_K!`iZDZ=8HH_jfs>>VB^?lu$+R{A zbeK^{3S03E!vMlV2ruYc8ctc{wj+@iJQ9y2CNTpITY$2e-w1p4}kvS&{<3wi=j4;mw?BIFa+Y) zZ@?6;VLE>Y)VoyJ|4ZRh`^hpg`xZp@x6dcCoK^haORUXu9<*P(f-&+XpNvK(WL`~b zgO1K?R52{I7I;lWkix8{FImQH>0_h#Zds0w#g{2wzGDZNdyw?5p~Lf5mMa+me`x!n zBiXrX1o?x>)g*s*8m-b`+u|kU?}Y`y5H3zwmUdvvu{>d(!eW`_3C?8D;-V~zDIEel z#)gmSr{FgZH09M9;8Q6mWQ-cZi8@AvW1*pKvh-dUG>a8K;z+s?1RdgKp#T|}16@!y zTfy>poF5q%`8ObFm5Q4$qX}SHZb>uyqwF;!S9}V>xR$+CYN+-TdmcwNO2tWB9wg$h zh7nmbZ6C7CBpC7=hRf`sklAM&i#lJ-i8ECtVkEzm?{?FKKsb4|Jy6NyWfp@@wEj1Y zNLy!FiorT~3&=)*;w7mF@rbAqM=na52>P6w3=4!vQ2tme93sZBLZ2?~PfF1UTh0e< zRd%zfmx&p6*_6G6&Xm1u7>i{-CG1Rl2*ojk23R`g>ZaJV$tg@{6X3{i$dAtO!ng485?w1j_upUJzSsHKAN51|l@!KOI)BWN2{GQR4I|^Q=_T^*MnB&k%U3zdp3e}}OJs!?l4!^|a13zE{LL7Z`nzs>}f zNP2cED-kp7wO^i%L}2e&h$uW*_9Uiq*k7HDDvC(7%`kWVl8^1MS}AA}(h+P@<1X7q zGB2YJ>?ayN|3h5V?IJOzviyRYDo3R6%xn3f$Ef|L>tSK34WA!GU zg-{{F)@hE1kl#ND+lI_LOq71M^UZoJ9{hfj&`#V1w=mDnsy9%TMw)IyE9oUHa1i1u47_*QMvhNg-!@B3N8lw-3sA7-o$%=^6wxXmv{+nSa1Ix{qxx!*!nU)XJfuPN*|e(Ci#ej`L$q&5i{9>YRR zl$5YJ(kRdLTGNce=n7C;0bIlbQnX$x;Rb(yMWB5bj>2Z#@+!qX%Hp`&@bDuGnoGKf zYu4@K6t~^QowM(6PZ5_N{*CL;G~b*2uyN3+RlLdfz^S5E_9p*Ex9%3Ch;8UF z2?`ufO21kKf#4S|2nw9+Caz%)96v8K%((KMtxznoJIc+vlybmgzGk>0cXiLWot;my zmCA7-LYOgkwqE#an1dik+D+96&LBdU^+I;IlRgq##2Vn-w)i5+k%YafKmq2#o* z85wBDlllBdVN4cqvKZPQOGJd2+!Zp&#P~1Ygy!pu99Mlm zn63_f{d8sHBIowyHBYN+|7PIYK)R{-aR3_leC9H~7pnU*)qUyeJ{o999nHPJhSy>_ zAc1cg{o~zkZ}&>>^OgRda?j^Y8+tg;=bbLN{|Bxd+F*@@DM~{5>D~l8beDAx$U(2} zONcW3E1~}a{Ix(|-PeD(`}Kj5@JRo$p@Hs^{$ahsC<6=!WvR>ugmGcCgXGK?YW(|-p2Rin#a9x z!8;x3Pgk3t#b&Oi35NMlAZUAAxyn`7cYWb)NP8P-?Yc~DFkRa>_k+xiK9ES=hD=>Y zy6)Jc{h6J|QhwUlmTBCbZan$8CG*{rshU*HAJ_;B(t=3UEc*TB;1>=}BUfFUdina6 z8+#V2HfE|e-a2{f?H^omLN`$kMQ+-0Q+ zYnd?%WJUnjr}fA4XqE-`faqSpEeEBmRD)Z_ZC|5mSF&DMj&Va2xmJ*>yOa-*B9N~+6fP~N6MgW9l-n66CN0_ z0S7<>1NP9m?Xz~E;YWH<-E4&k8z_=B`=0+7@CL4==R@W&bHY%YU= z^#~6Q!0)AXZ!vlp0}Rq&yY-|o3h8M@cxV9rI55!32zrG!g=v#QTh7v!DFDht+6O=v za#%+h6~GyYCCaGoE#eZza)8$FqxA=n>_2A%G&gNIQmCS^XWCWgUgWS%bE_B6dv>`5 Nh<3}hQZLx*e*qj*x=#QA literal 0 HcmV?d00001 diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index aaf076f15cb7c3c75c77c7212d05164163696852..9b92d9c9845fee06b064631c3df1dc09f4dc61ee 100644 GIT binary patch delta 10727 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 delta 10399 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 MWaY1l`5NPY0Gs=;$^ZZW diff --git a/core/__pycache__/whatsapp_utils.cpython-311.pyc b/core/__pycache__/whatsapp_utils.cpython-311.pyc index 81f17cc9c6a3e9a5ec486e4b2964cc48221430a6..4a862f3fe01777452e2f76db3592acf5256c0495 100644 GIT binary patch delta 5546 zcmbtYS!^4}8J;C~m&^N-NQojTTBqsQk`v#t<3_UUL$T%hNDw==X-Z4W)Ill3l_j^N z$p9&!qHSPfTvSC}Ko5oMA`TM3K+zU$;Uq79$ip&NH0%Nj1QdR0P=Enyw8&Hc8ImhX zR?-$7?S7iwnfd3~@Bii>{$%2hmtDVdI&BEj{LGE?&CR!6E!=Nz_nse-1zDPOO}Q_) zIb=d9ZrnpF(UkJu;}Cim?rOT=ldW%~3x3%K*d*Hln`H-JKz0HKWfx$J>;`O=J%AyZ zJB#Ap+kd_gj&pj;SV~D`rj_*7^k`B^PiGRN(-|d|Q66sRUN>|0hd zKb#kT$#F3fk=lgKU~SvOm!#hboP+$+`E9QI;Y+R=&f5a-bv~WBI*nk^L9N}YAs1)1y@ zMDu*#{KVHxE_=6_SL|N$V{gp6;`FPwp6XRRwBl1hYeg zIXcAR;fk%5@V=hVvcK)lOH2OyW4}H8xI^`yEcs7r{*wz*g(ru7``Sh1XwvNMOFKTb z_o()sWq0f1w)dJ!?mo@kS3wqX*Eelm3AR1-__uP0RQS|CMG9a&F4Mdrkr`>+OqIKya#sP zHEeMxv$GQ^jKoztT?qH)1;w#yb;*Jdxo`Dz77{kMJ}>2kn?gpC+5b6i4$Yg-qd5+S zZLl1nabr|VHZoxHla(HKAur}c+?5l@?RDe0U1_c75o#=vt#m4~Eyu%z_n1aSGX*E6Gr?{Obk*@03K9iMYfKrBk5YhDLYOWA_EKPMr+yN* z=w|3dw=mb7SMWJ_soPRh$@FAmWLmzVJLj$?m27fmhCO0Rd>jSHTF_~O{*3jyk2(8| zL{gqgXA;TD6jty>s5(olp8}Q@(Ni1RGRQsZd^D$?zof=5m*ST-K)$DVbm78cgbV~< z;5vvBoZtikG<}V;rnp;kTJX6FqS<9<$I>3HbC0@j^l=b?Ugwh4Ovx#0PFbyA6*Fy6 zZPtkYdSY)PXInbAEDmr3A@pI$HfT40*cBNR%^!&Z%G#2N2Bv&zrt8h)B{JPfC92(9vXjLVrH@atVm7C@@S;W)4J zDqzvmgsx;SYgSf3Hk5O-9@&M5c8m{+;~Ro z$y@T0LZzvepaa8)a?;usps)zIYZ+$b(}Od~8;r5p+0oHdHoMB$0vKbI1(G{)YBZI; zp2CStaw>(d(V{C980}jP+Fws+(<76qM7?_4g-2+$Q3^7JD-`JZRCRYP+woOeKwTD3 zBj_KeAgjCT))F($Rilog9}V?QbUCr9z2P)uvxR&MF~I?ryQp4}p;`eL(ezksKJ`PJkKJO2hT6n7G zWUS`|pEoO3>P0_!7^RywOR|Z$dn1R0`t!IE^nfeVy}Xth+l+5eJ7*!&y^(?uAc33H0Jh#* z$cbw#81=x-g$Kg=Jz~wYO?2Y22(y*e%+|8rKQEer_<1Y*+13r+Wemi&-lf-dg~n-6 z!xEGju^Z$Kk&vakLf*b+DAt^v*=;tWF}CuKyfg2reqs|W^u<_j^kkz+t^&B}TNrYMoImZS|#aPk664RQo+F-11g`O{JNw(+Q>(_wEnqwH# zufq1*IA4`GyNnI#(mjRIBu=pSFp-&^8iBo;X;^cu(oA}EVs^%m6S`&B>{L~6j#{4w z&;x5*)CDFkR5gytldEPfDa#ndRrkm#vVUuHL2p{yxcUT54fCr;aGv(o+7K2dYK@bi z*fUDp5AkOb<1|cVRcD#(ve1@EEFE#xB$d=cRe`(c%gq#4b@xsB_&o|a3bbz)g~B0^ zsV*BJo14{GNQ|MDF4B-Bk<~5Lpd_pFAhKDUr52dz(r2u@K#>bdYF??$qSj8Xx06{r zS$Eczn@#-dPm6k>7B|-yC7Vr;Wl}Q!A$0r++}T0ka}hz1bEoe!U*D&`z7qM%*6jrZ z$K>P7&+7Dna#QHeu-3FiYudkHtw<=`L1Uy~M>*X6Y@`&Vddl_!2a4=ccb#}VT?${; z!Vs0W7LP0(y>)Jbt>8EWO@+p&>G-;+X=h0~rb)+C>DcOCzzG?s6weZDnn)gscuvnmb z;t;oNjV@lldsA!QrL_+}I$W|I*R01?>+%12-YuQ<`&8pun0;-S&2*T}V(}ylQwXZT z7d|*y;s-T;P<6u1E*4hOO)ZdN2YOH*U`;jU&=OaV#}2~j#FXb0W8YvQCbw3GR?qs1 zvE@O42nQWGH973&$X*H`TniHipq_a86I*cc4guuaW%_GDWnq;W0%p(_;z%98)9~ zh@DbTzg~)-*P`dw1!9H&I}+oBN#=e0{dA-EjjPf`Nt)243CKLD2Tp+!zgOe;s`U#8 zXy6oY!w2bB#l^0*+4#BSu4LHv^xqTJ%EU=mV8%4@1`~Uy| literal 13717 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 o=wOYqL~~r_d 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 ZYlO!G;fByn9z|>M3op6g^)XTp_a9nHT=W0{ literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 6fca213..6545e00 100644 --- a/core/models.py +++ b/core/models.py @@ -294,4 +294,41 @@ class DriverRating(models.Model): class Meta: verbose_name = _('Driver Rating') - verbose_name_plural = _('Driver Ratings') \ No newline at end of file + verbose_name_plural = _('Driver Ratings') + +class NotificationTemplate(models.Model): + KEY_CHOICES = ( + ('otp_registration', 'OTP Registration'), + ('otp_login', 'OTP Login'), + ('otp_profile_update', 'OTP Profile Update'), + ('shipment_created_shipper', 'Shipment Created (Shipper)'), + ('payment_success_shipper', 'Payment Success (Shipper)'), + ('shipment_visible_receiver', 'Shipment Visible (Receiver)'), + ('driver_pickup_shipper', 'Driver Pickup (Shipper)'), + ('driver_pickup_receiver', 'Driver Pickup (Receiver)'), + ('driver_pickup_driver', 'Driver Pickup (Driver/Carrier)'), + ('shipment_status_update', 'Shipment Status Update'), + ('admin_alert_driver_accept', 'Admin Alert: Driver Accepted'), + ('contact_form_admin', 'Contact Form (Admin)'), + ) + + key = models.CharField(max_length=50, choices=KEY_CHOICES, unique=True) + description = models.CharField(max_length=255, help_text="Description of where this notification is used.") + available_variables = models.TextField(help_text="Comma-separated list of variables available in this template (e.g. {{ code }}, {{ name }}).", blank=True) + + # Email + subject_en = models.CharField(max_length=255, blank=True, verbose_name="Email Subject (EN)") + subject_ar = models.CharField(max_length=255, blank=True, verbose_name="Email Subject (AR)") + email_body_en = models.TextField(blank=True, verbose_name="Email Body (EN)", help_text="HTML allowed.") + email_body_ar = models.TextField(blank=True, verbose_name="Email Body (AR)", help_text="HTML allowed.") + + # WhatsApp + whatsapp_body_en = models.TextField(blank=True, verbose_name="WhatsApp Message (EN)") + whatsapp_body_ar = models.TextField(blank=True, verbose_name="WhatsApp Message (AR)") + + def __str__(self): + return f"{self.get_key_display()}" + + class Meta: + verbose_name = _('Notification Template') + verbose_name_plural = _('Notification Templates') \ No newline at end of file diff --git a/core/notifications.py b/core/notifications.py new file mode 100644 index 0000000..dd62817 --- /dev/null +++ b/core/notifications.py @@ -0,0 +1,183 @@ +from django.utils.translation import get_language +from .models import NotificationTemplate +from django.template import Template, Context +import logging + +logger = logging.getLogger(__name__) + +DEFAULT_TEMPLATES = { + 'otp_registration': { + 'description': 'Sent when a user registers (Email/WhatsApp)', + 'variables': '{{ code }}', + 'subject_en': 'Verification Code', + 'subject_ar': 'رمز التحقق', + 'email_body_en': 'Your Masar Verification Code is {{ code }}', + 'email_body_ar': 'رمز التحقق الخاص بك هو {{ code }}', + 'whatsapp_body_en': 'Your Masar Verification Code is {{ code }}', + 'whatsapp_body_ar': 'رمز التحقق الخاص بك هو {{ code }}', + }, + 'otp_login': { + 'description': 'Sent for 2FA Login (Email/WhatsApp)', + 'variables': '{{ code }}', + 'subject_en': 'Login OTP', + 'subject_ar': 'رمز الدخول', + 'email_body_en': 'Your Masar Login Code is {{ code }}. Do not share this code.', + 'email_body_ar': 'رمز دخول مسار هو {{ code }}. لا تشارك هذا الرمز.', + 'whatsapp_body_en': 'Your Masar Login Code is {{ code }}. Do not share this code.', + 'whatsapp_body_ar': 'رمز دخول مسار هو {{ code }}. لا تشارك هذا الرمز.', + }, + 'otp_profile_update': { + 'description': 'Sent when updating profile sensitive info', + 'variables': '{{ code }}', + 'subject_en': 'Verification Code', + 'subject_ar': 'رمز التحقق', + 'email_body_en': 'Your Masar Update Code is {{ code }}', + 'email_body_ar': 'رمز التحديث الخاص بك هو {{ code }}', + 'whatsapp_body_en': 'Your Masar Update Code is {{ code }}', + 'whatsapp_body_ar': 'رمز التحديث الخاص بك هو {{ code }}', + }, + 'shipment_created_shipper': { + 'description': 'Sent to Shipper when they create a shipment', + 'variables': '{{ name }}, {{ description }}, {{ tracking_number }}, {{ status }}', + 'subject_en': 'Shipment Request Received - {{ tracking_number }}', + 'subject_ar': 'تم استلام طلب الشحنة - {{ tracking_number }}', + 'email_body_en': "Hello {{ name }},\n\nYour shipment request for '{{ description }}' has been received.\nTracking Number: {{ tracking_number }}\nStatus: {{ status }}\n\nPlease proceed to payment to make it visible to drivers.", + 'email_body_ar': "مرحباً {{ name }}،\n\nتم استلام طلب الشحنة '{{ description }}'.\nرقم التتبع: {{ tracking_number }}\nالحالة: {{ status }}\n\nيرجى متابعة الدفع لجعلها مرئية للسائقين.", + 'whatsapp_body_en': "Hello {{ name }},\nYour shipment request for '{{ description }}' has been received.\nTracking Number: {{ tracking_number }}\nStatus: {{ status }}\nPlease proceed to payment.", + 'whatsapp_body_ar': "مرحباً {{ name }}،\nتم استلام طلب الشحنة '{{ description }}'.\nرقم التتبع: {{ tracking_number }}\nالحالة: {{ status }}\nيرجى الدفع.", + }, + 'payment_success_shipper': { + 'description': 'Sent to Shipper after payment', + 'variables': '{{ tracking_number }}', + 'subject_en': 'Payment Successful - {{ tracking_number }}', + 'subject_ar': 'تم الدفع بنجاح - {{ tracking_number }}', + 'email_body_en': 'Payment successful for shipment {{ tracking_number }}.\nYour shipment is now visible to available drivers.', + 'email_body_ar': 'تم الدفع بنجاح للشحنة {{ tracking_number }}.\nشحنتك الآن مرئية للسائقين المتاحين.', + 'whatsapp_body_en': 'Payment successful for shipment {{ tracking_number }}.\nYour shipment is now visible to available drivers.', + 'whatsapp_body_ar': 'تم الدفع بنجاح للشحنة {{ tracking_number }}.\nشحنتك الآن مرئية للسائقين المتاحين.', + }, + 'shipment_visible_receiver': { + 'description': 'Sent to Receiver when shipment is paid/ready', + 'variables': '{{ receiver_name }}, {{ shipper_name }}, {{ tracking_number }}, {{ status }}', + 'subject_en': 'Incoming Shipment - {{ tracking_number }}', + 'subject_ar': 'شحنة واردة - {{ tracking_number }}', + 'email_body_en': 'Hello {{ receiver_name }},\n\nA shipment is coming your way from {{ shipper_name }}.\nTracking Number: {{ tracking_number }}\nStatus: {{ status }}', + 'email_body_ar': 'مرحباً {{ receiver_name }}،\n\nشحنة قادمة إليك من {{ shipper_name }}.\nرقم التتبع: {{ tracking_number }}\nالحالة: {{ status }}', + 'whatsapp_body_en': 'Hello {{ receiver_name }},\nA shipment is coming your way from {{ shipper_name }}.\nTracking Number: {{ tracking_number }}\nStatus: {{ status }}', + 'whatsapp_body_ar': 'مرحباً {{ receiver_name }}،\nشحنة قادمة إليك من {{ shipper_name }}.\nرقم التتبع: {{ tracking_number }}\nالحالة: {{ status }}', + }, + 'driver_pickup_shipper': { + 'description': 'Sent to Shipper when driver picks up', + 'variables': '{{ tracking_number }}, {{ driver_name }}, {{ car_plate_number }}, {{ status }}', + 'subject_en': 'Driver Assigned - {{ tracking_number }}', + 'subject_ar': 'تم تعيين سائق - {{ tracking_number }}', + 'email_body_en': 'Shipment {{ tracking_number }} has been picked up by {{ driver_name }}.\nCar Plate: {{ car_plate_number }}\nStatus: {{ status }}', + 'email_body_ar': 'الشحنة {{ tracking_number }} تم استلامها بواسطة {{ driver_name }}.\nرقم اللوحة: {{ car_plate_number }}\nالحالة: {{ status }}', + 'whatsapp_body_en': 'Shipment {{ tracking_number }} has been picked up by {{ driver_name }}.\nCar Plate: {{ car_plate_number }}\nStatus: {{ status }}', + 'whatsapp_body_ar': 'الشحنة {{ tracking_number }} تم استلامها بواسطة {{ driver_name }}.\nرقم اللوحة: {{ car_plate_number }}\nالحالة: {{ status }}', + }, + 'driver_pickup_receiver': { + 'description': 'Sent to Receiver when driver picks up', + 'variables': '{{ tracking_number }}, {{ shipper_name }}, {{ driver_name }}, {{ car_plate_number }}', + 'subject_en': 'Shipment On The Way - {{ tracking_number }}', + 'subject_ar': 'الشحنة في الطريق - {{ tracking_number }}', + 'email_body_en': 'Shipment {{ tracking_number }} from {{ shipper_name }} is on the way (Picked up).\nDriver: {{ driver_name }}\nCar Plate: {{ car_plate_number }}', + 'email_body_ar': 'الشحنة {{ tracking_number }} من {{ shipper_name }} في الطريق (تم الاستلام).\nالسائق: {{ driver_name }}\nرقم اللوحة: {{ car_plate_number }}', + 'whatsapp_body_en': 'Shipment {{ tracking_number }} from {{ shipper_name }} is on the way (Picked up).\nDriver: {{ driver_name }}\nCar Plate: {{ car_plate_number }}', + 'whatsapp_body_ar': 'الشحنة {{ tracking_number }} من {{ shipper_name }} في الطريق (تم الاستلام).\nالسائق: {{ driver_name }}\nرقم اللوحة: {{ car_plate_number }}', + }, + 'driver_pickup_driver': { + 'description': 'Sent to Driver upon acceptance', + 'variables': '{{ tracking_number }}, {{ shipper_name }}, {{ pickup_address }}, {{ delivery_address }}, {{ price }}', + 'subject_en': 'Shipment Accepted - {{ tracking_number }}', + 'subject_ar': 'تم قبول الشحنة - {{ tracking_number }}', + 'email_body_en': 'You have successfully accepted Shipment {{ tracking_number }}.\nShipper: {{ shipper_name }}\nPickup: {{ pickup_address }}\nDelivery: {{ delivery_address }}\nPrice: {{ price }} OMR', + 'email_body_ar': 'لقد قبلت الشحنة {{ tracking_number }} بنجاح.\nالشاحن: {{ shipper_name }}\nالاستلام: {{ pickup_address }}\nالتوصيل: {{ delivery_address }}\nالسعر: {{ price }} ر.ع', + 'whatsapp_body_en': 'You have successfully accepted Shipment {{ tracking_number }}.\nShipper: {{ shipper_name }}\nPickup: {{ pickup_address }}\nDelivery: {{ delivery_address }}\nPrice/Bid: {{ price }} OMR', + 'whatsapp_body_ar': 'لقد قبلت الشحنة {{ tracking_number }} بنجاح.\nالشاحن: {{ shipper_name }}\nالاستلام: {{ pickup_address }}\nالتوصيل: {{ delivery_address }}\nالسعر: {{ price }} ر.ع', + }, + 'shipment_status_update': { + 'description': 'Sent on general status change (In Transit, Delivered)', + 'variables': '{{ tracking_number }}, {{ status }}', + 'subject_en': 'Shipment Update - {{ tracking_number }}', + 'subject_ar': 'تحديث الشحنة - {{ tracking_number }}', + 'email_body_en': 'Update for shipment {{ tracking_number }}:\nNew Status: {{ status }}', + 'email_body_ar': 'تحديث للشحنة {{ tracking_number }}:\nالحالة الجديدة: {{ status }}', + 'whatsapp_body_en': 'Update for shipment {{ tracking_number }}:\nNew Status: {{ status }}', + 'whatsapp_body_ar': 'تحديث للشحنة {{ tracking_number }}:\nالحالة الجديدة: {{ status }}', + }, + 'admin_alert_driver_accept': { + 'description': 'Sent to Admin when driver accepts shipment', + 'variables': '{{ driver_name }}, {{ car_plate_number }}, {{ tracking_number }}, {{ shipper_name }}, {{ price }}', + 'subject_en': 'Shipment Accepted ({{ tracking_number }})', + 'subject_ar': 'تم قبول الشحنة ({{ tracking_number }})', + 'email_body_en': 'Driver {{ driver_name }} ({{ car_plate_number }}) accepted shipment {{ tracking_number }} from {{ shipper_name }}.\nPrice: {{ price }} OMR', + 'email_body_ar': 'قام السائق {{ driver_name }} ({{ car_plate_number }}) بقبول الشحنة {{ tracking_number }} من {{ shipper_name }}.\nالسعر: {{ price }} ر.ع', + 'whatsapp_body_en': 'Driver {{ driver_name }} ({{ car_plate_number }}) accepted shipment {{ tracking_number }} from {{ shipper_name }}.\nPrice: {{ price }} OMR', + 'whatsapp_body_ar': 'قام السائق {{ driver_name }} ({{ car_plate_number }}) بقبول الشحنة {{ tracking_number }} من {{ shipper_name }}.\nالسعر: {{ price }} ر.ع', + }, + 'contact_form_admin': { + 'description': 'Sent to Admin when contact form is submitted', + 'variables': '{{ name }}, {{ email }}, {{ message }}', + 'subject_en': 'New Contact Message from {{ name }}', + 'subject_ar': 'رسالة جديدة من {{ name }}', + 'email_body_en': 'You have received a new message from your website contact form.\n\nName: {{ name }}\nEmail: {{ email }}\n\nMessage:\n{{ message }}', + 'email_body_ar': 'لقد تلقيت رسالة جديدة من نموذج الاتصال.\n\nالاسم: {{ name }}\nالبريد: {{ email }}\n\nالرسالة:\n{{ message }}', + 'whatsapp_body_en': 'New Message from {{ name }}:\n{{ message }}', + 'whatsapp_body_ar': 'رسالة جديدة من {{ name }}:\n{{ message }}', + } +} + +def get_notification_content(key, context, language=None): + if not language: + language = get_language() or 'en' + + # 1. Fetch or Create Template + try: + template_obj = NotificationTemplate.objects.get(key=key) + except NotificationTemplate.DoesNotExist: + # Create default + default = DEFAULT_TEMPLATES.get(key) + if default: + template_obj = NotificationTemplate.objects.create( + key=key, + description=default.get('description', ''), + available_variables=default.get('variables', ''), + subject_en=default.get('subject_en', ''), + subject_ar=default.get('subject_ar', ''), + email_body_en=default.get('email_body_en', ''), + email_body_ar=default.get('email_body_ar', ''), + whatsapp_body_en=default.get('whatsapp_body_en', ''), + whatsapp_body_ar=default.get('whatsapp_body_ar', ''), + ) + else: + # Fallback if key unknown + return f"[{key}] Subject", f"[{key}] Body", f"[{key}] WA" + + # 2. Select Language Fields + # Note: If translation is missing, fallback to EN + if language == 'ar': + subject = template_obj.subject_ar or template_obj.subject_en + email_body = template_obj.email_body_ar or template_obj.email_body_en + whatsapp_body = template_obj.whatsapp_body_ar or template_obj.whatsapp_body_en + else: + subject = template_obj.subject_en + email_body = template_obj.email_body_en + whatsapp_body = template_obj.whatsapp_body_en + + # 3. Render + # Use Django Template engine for variable substitution + + def render(text, ctx): + if not text: return "" + try: + # Convert context to dict if it isn't already + if not isinstance(ctx, dict): + ctx = {} + t = Template(text) + return t.render(Context(ctx)) + except Exception as e: + logger.error(f"Template rendering error for {key}: {e}") + return text + + return render(subject, context), render(email_body, context), render(whatsapp_body, context) \ No newline at end of file diff --git a/core/views.py b/core/views.py index aea9bc3..fc3c617 100644 --- a/core/views.py +++ b/core/views.py @@ -20,6 +20,7 @@ from django.db.models import Avg, Count from django.template.loader import render_to_string import random import string +from .notifications import get_notification_content from .whatsapp_utils import ( notify_shipment_created, notify_payment_received, @@ -100,16 +101,16 @@ def register_shipper(request): # Send OTP method = form.cleaned_data.get('verification_method', 'email') - otp_msg = _("Your Masar Verification Code is %(code)s") % {'code': code} + subj, email_msg, wa_msg = get_notification_content('otp_registration', {'code': code}, language=get_language()) if method == 'whatsapp': phone = user.profile.phone_number - send_whatsapp_message(phone, otp_msg) + send_whatsapp_message(phone, wa_msg) messages.info(request, _("Verification code sent to WhatsApp.")) else: send_html_email( - subject=_('Verification Code'), - message=otp_msg, + subject=subj, + message=email_msg, recipient_list=[user.email], title=_('Welcome to Masar!'), request=request @@ -143,16 +144,16 @@ def register_driver(request): # Send OTP method = form.cleaned_data.get('verification_method', 'email') - otp_msg = _("Your Masar Verification Code is %(code)s") % {'code': code} + subj, email_msg, wa_msg = get_notification_content('otp_registration', {'code': code}, language=get_language()) if method == 'whatsapp': phone = user.profile.phone_number - send_whatsapp_message(phone, otp_msg) + send_whatsapp_message(phone, wa_msg) messages.info(request, _("Verification code sent to WhatsApp.")) else: send_html_email( - subject=_('Verification Code'), - message=otp_msg, + subject=subj, + message=email_msg, recipient_list=[user.email], title=_('Welcome to Masar!'), request=request @@ -489,22 +490,22 @@ def edit_profile(request): # 4. Send OTP method = data.get('otp_method', 'email') - otp_msg = _("Your Masar Update Code is %(code)s") % {'code': code} + subj, email_msg, wa_msg = get_notification_content('otp_profile_update', {'code': code}, language=get_language()) if method == 'whatsapp': # Use current phone if available, else new phone phone = request.user.profile.phone_number or data['phone_number'] - send_whatsapp_message(phone, otp_msg) + send_whatsapp_message(phone, wa_msg) messages.info(request, _("Verification code sent to WhatsApp.")) else: # Default to email # Send to the NEW email address (from the form), not the old one target_email = data['email'] send_html_email( - subject=_('Verification Code'), - message=otp_msg, + subject=subj, + message=email_msg, recipient_list=[target_email], - title=_('Profile Update Verification'), + title=subj, request=request ) messages.info(request, _("Verification code sent to email.")) @@ -641,20 +642,21 @@ def request_login_otp(request): # Generate OTP code = ''.join(random.choices(string.digits, k=6)) + subj, email_msg, wa_msg = get_notification_content('otp_login', {'code': code}, language=get_language()) OTPVerification.objects.create(user=user, code=code, purpose='login') # Send OTP - otp_msg = _("Your Masar Login Code is %(code)s. Do not share this code.") % {'code': code} + subj, email_msg, wa_msg = get_notification_content('otp_login', {'code': code}, language=get_language()) try: if method == 'whatsapp': phone = user.profile.phone_number - send_whatsapp_message(phone, otp_msg) + send_whatsapp_message(phone, wa_msg) message_sent = _("OTP sent to your WhatsApp.") else: send_html_email( - subject=_('Login OTP'), - message=otp_msg, + subject=subj, + message=email_msg, recipient_list=[user.email], title=_('Login Verification'), request=request @@ -983,6 +985,7 @@ def select_2fa_method(request): if request.method == 'POST': method = request.POST.get('method') code = ''.join(random.choices(string.digits, k=6)) + subj, email_msg, wa_msg = get_notification_content('otp_login', {'code': code}, language=get_language()) # Invalidate old login OTPs OTPVerification.objects.filter(user=user, purpose='login').delete() @@ -993,7 +996,7 @@ def select_2fa_method(request): try: send_html_email( subject=_("Your Login OTP"), - message=f"Your verification code is: {code}", + message=email_msg, recipient_list=[user.email], title=_("Login Verification") ) @@ -1006,7 +1009,7 @@ def select_2fa_method(request): elif method == 'whatsapp': if hasattr(user, 'profile') and user.profile.phone_number: - if send_whatsapp_message(user.profile.phone_number, f"Your login verification code is: {code}"): + if send_whatsapp_message(user.profile.phone_number, wa_msg): messages.success(request, _("OTP sent to your WhatsApp.")) return redirect('verify_2fa_otp') else: diff --git a/core/whatsapp_utils.py b/core/whatsapp_utils.py index 94d6fb5..07d5ef0 100644 --- a/core/whatsapp_utils.py +++ b/core/whatsapp_utils.py @@ -6,6 +6,7 @@ from django.core.mail import send_mail from django.utils.translation import gettext_lazy as _ from .models import PlatformProfile from .mail import send_html_email +from .notifications import get_notification_content logger = logging.getLogger(__name__) @@ -16,7 +17,6 @@ def get_whatsapp_credentials(): """ # Defaults api_token = settings.WHATSAPP_API_KEY if hasattr(settings, 'WHATSAPP_API_KEY') else "" - # We repurpose Phone ID as Domain in settings if needed, or default to Wablas DEU domain = "https://deu.wablas.com" secret_key = "" source = "Settings/Env" @@ -25,19 +25,15 @@ def get_whatsapp_credentials(): try: profile = PlatformProfile.objects.first() if profile: - # Check for token override if profile.whatsapp_access_token: api_token = profile.whatsapp_access_token.strip() source = "Database (PlatformProfile)" - # Check for secret key override if profile.whatsapp_app_secret: secret_key = profile.whatsapp_app_secret.strip() - # Check for domain override if profile.whatsapp_business_phone_number_id: domain = profile.whatsapp_business_phone_number_id.strip() - # Ensure no trailing slash if domain.endswith('/'): domain = domain[:-1] @@ -71,28 +67,21 @@ def send_whatsapp_message_detailed(phone_number, message): logger.warning(msg) return False, msg - # Normalize phone number (Wablas expects international format without +, e.g. 628123...) clean_phone = str(phone_number).replace('+', '').replace(' ', '') - # Endpoint: /api/send-message (Simple Text) - # Ensure domain has schema if not domain.startswith('http'): domain = f"https://{domain}" - # Using the exact endpoint provided in user example url = f"{domain}/api/send-message" - # Header construction logic from user example auth_header = api_token if secret_key: auth_header = f"{api_token}.{secret_key}" headers = { "Authorization": auth_header, - # requests will set Content-Type to application/x-www-form-urlencoded when using 'data' param } - # Payload as form data (not JSON) data = { "phone": clean_phone, "message": message, @@ -100,18 +89,14 @@ def send_whatsapp_message_detailed(phone_number, message): try: logger.info(f"Attempting to send WhatsApp message to {clean_phone} via {url}") - # Use data=data for form-urlencoded response = requests.post(url, headers=headers, data=data, timeout=15) - # Handle non-JSON response (HTML error pages) try: response_data = response.json() except ValueError: response_data = response.text - # Wablas success usually has status: true if response.status_code == 200: - # Check for logical success in JSON if isinstance(response_data, dict): if response_data.get('status') is True: logger.info(f"WhatsApp message sent to {clean_phone} via Wablas") @@ -119,7 +104,6 @@ def send_whatsapp_message_detailed(phone_number, message): else: return False, f"Wablas API Logic Error (Source: {source}): {response_data}" else: - # If text, assume success if 200 OK? Or inspect text. return True, f"Message sent (Raw Response). (Source: {source})" else: error_msg = f"Wablas API error (Source: {source}): {response.status_code} - {response_data}" @@ -130,14 +114,19 @@ def send_whatsapp_message_detailed(phone_number, message): logger.error(error_msg) return False, error_msg -def notify_admin(subject, message): - """Notifies the admin via Email and WhatsApp (if configured in PlatformProfile).""" +def notify_admin_alert(key, context): + """Notifies the admin via Email and WhatsApp using a template key.""" + # 1. Get Template Content (Admin likely prefers EN, or system default?) + # Let's force EN for admin alerts for now, or check generic language setting? + # Usually admin alerts are in EN or the default site language. + subject, email_body, whatsapp_body = get_notification_content(key, context, language='en') + # Email try: if hasattr(settings, 'CONTACT_EMAIL_TO') and settings.CONTACT_EMAIL_TO: send_html_email( subject=f"Admin Alert: {subject}", - message=message, + message=email_body, recipient_list=settings.CONTACT_EMAIL_TO, title="Admin Alert" ) @@ -148,146 +137,141 @@ def notify_admin(subject, message): try: profile = PlatformProfile.objects.first() if profile and profile.phone_number: - # Assuming profile.phone_number is a valid WhatsApp number for Admin alerts - send_whatsapp_message(profile.phone_number, f"ADMIN ALERT: {subject}\n{message}") + send_whatsapp_message(profile.phone_number, f"ADMIN ALERT: {subject}\n{whatsapp_body}") except Exception: pass def notify_shipment_created(parcel): - """Notifies the shipper that the shipment request was received via WhatsApp and Email.""" shipper_name = parcel.shipper.get_full_name() or parcel.shipper.username - message = f"""Hello {shipper_name}, - -Your shipment request for '{parcel.description}' has been received. -Tracking Number: {parcel.tracking_number} -Status: {parcel.get_status_display()} - -Please proceed to payment to make it visible to drivers.""" + context = { + 'name': shipper_name, + 'description': parcel.description, + 'tracking_number': parcel.tracking_number, + 'status': parcel.get_status_display() + } + # Render for Shipper (check user language preference? For now assume session/request unavailable so maybe default or EN, + # OR we need a user language profile field. The user didn't ask for user-pref language yet, just bilingual templates. + # I'll default to EN unless I can guess.) + # Actually, if I can't determine, EN is safe. + # Future improvement: Add language to User Profile. + + subj, email_msg, wa_msg = get_notification_content('shipment_created_shipper', context) + # WhatsApp if hasattr(parcel.shipper, 'profile') and parcel.shipper.profile.phone_number: - send_whatsapp_message(parcel.shipper.profile.phone_number, message) - else: - logger.warning(f"No phone number found for shipper {shipper_name}, skipping WhatsApp.") + send_whatsapp_message(parcel.shipper.profile.phone_number, wa_msg) # Email if parcel.shipper.email: - try: - send_html_email( - subject='Shipment Request Received - ' + parcel.tracking_number, - message=message, - recipient_list=[parcel.shipper.email], - title='Shipment Request Received' - ) - logger.info(f"Shipment created email sent to {parcel.shipper.email}") - except Exception as e: - logger.error(f"Failed to send shipment created email to {parcel.shipper.email}: {e}") - + send_html_email( + subject=subj, + message=email_msg, + recipient_list=[parcel.shipper.email], + title=subj + ) return True def notify_payment_received(parcel): - """Notifies the shipper and receiver about successful payment via WhatsApp and Email.""" - # Notify Shipper + # Shipper shipper_name = parcel.shipper.get_full_name() or parcel.shipper.username - shipper_msg = f"""Payment successful for shipment {parcel.tracking_number}. -Your shipment is now visible to available drivers.""" + context_shipper = { + 'tracking_number': parcel.tracking_number + } + subj, email_msg, wa_msg = get_notification_content('payment_success_shipper', context_shipper) - # WhatsApp Shipper if hasattr(parcel.shipper, 'profile') and parcel.shipper.profile.phone_number: - send_whatsapp_message(parcel.shipper.profile.phone_number, shipper_msg) + send_whatsapp_message(parcel.shipper.profile.phone_number, wa_msg) - # Email Shipper if parcel.shipper.email: - try: - send_html_email( - subject='Payment Successful - ' + parcel.tracking_number, - message=shipper_msg, - recipient_list=[parcel.shipper.email], - title='Payment Successful' - ) - except Exception as e: - logger.error(f"Failed to send payment email to {parcel.shipper.email}: {e}") + send_html_email( + subject=subj, + message=email_msg, + recipient_list=[parcel.shipper.email], + title=subj + ) - # Notify Receiver - receiver_msg = f"""Hello {parcel.receiver_name}, - -A shipment is coming your way from {shipper_name}. -Tracking Number: {parcel.tracking_number} -Status: {parcel.get_status_display()}""" - send_whatsapp_message(parcel.receiver_phone, receiver_msg) + # Receiver + context_receiver = { + 'receiver_name': parcel.receiver_name, + 'shipper_name': shipper_name, + 'tracking_number': parcel.tracking_number, + 'status': parcel.get_status_display() + } + _, _, wa_msg_rx = get_notification_content('shipment_visible_receiver', context_receiver) + send_whatsapp_message(parcel.receiver_phone, wa_msg_rx) def notify_driver_assigned(parcel): - """Notifies the shipper, receiver, driver, and admin that a driver has picked up the parcel.""" driver_name = parcel.carrier.get_full_name() or parcel.carrier.username shipper_name = parcel.shipper.get_full_name() or parcel.shipper.username + # Get Car Plate + car_plate = "" + if hasattr(parcel.carrier, 'profile'): + car_plate = parcel.carrier.profile.car_plate_number + # 1. Notify Shipper - msg_shipper = f"""Shipment {parcel.tracking_number} has been picked up by {driver_name}. -Status: {parcel.get_status_display()}""" + context_shipper = { + 'tracking_number': parcel.tracking_number, + 'driver_name': driver_name, + 'car_plate_number': car_plate, + 'status': parcel.get_status_display() + } + subj_s, email_s, wa_s = get_notification_content('driver_pickup_shipper', context_shipper) if hasattr(parcel.shipper, 'profile') and parcel.shipper.profile.phone_number: - send_whatsapp_message(parcel.shipper.profile.phone_number, msg_shipper) + send_whatsapp_message(parcel.shipper.profile.phone_number, wa_s) if parcel.shipper.email: - try: - send_html_email( - subject='Driver Assigned - ' + parcel.tracking_number, - message=msg_shipper, - recipient_list=[parcel.shipper.email], - title='Driver Assigned' - ) - except Exception: - pass + send_html_email(subject=subj_s, message=email_s, recipient_list=[parcel.shipper.email], title=subj_s) # 2. Notify Receiver - msg_receiver = f"""Shipment {parcel.tracking_number} from {shipper_name} is on the way (Picked up). -Driver: {driver_name}""" - send_whatsapp_message(parcel.receiver_phone, msg_receiver) + context_receiver = { + 'tracking_number': parcel.tracking_number, + 'shipper_name': shipper_name, + 'driver_name': driver_name, + 'car_plate_number': car_plate + } + _, _, wa_r = get_notification_content('driver_pickup_receiver', context_receiver) + send_whatsapp_message(parcel.receiver_phone, wa_r) - # 3. Notify Driver (Confirmation) - msg_driver = f"""You have successfully accepted Shipment {parcel.tracking_number}. -Shipper: {shipper_name} -Pickup: {parcel.pickup_address} -Delivery: {parcel.delivery_address} -Price/Bid: {parcel.price} OMR""" + # 3. Notify Driver + context_driver = { + 'tracking_number': parcel.tracking_number, + 'shipper_name': shipper_name, + 'pickup_address': parcel.pickup_address, + 'delivery_address': parcel.delivery_address, + 'price': parcel.price + } + subj_d, email_d, wa_d = get_notification_content('driver_pickup_driver', context_driver) if hasattr(parcel.carrier, 'profile') and parcel.carrier.profile.phone_number: - send_whatsapp_message(parcel.carrier.profile.phone_number, msg_driver) + send_whatsapp_message(parcel.carrier.profile.phone_number, wa_d) if parcel.carrier.email: - try: - send_html_email( - subject='Shipment Accepted - ' + parcel.tracking_number, - message=msg_driver, - recipient_list=[parcel.carrier.email], - title='Shipment Accepted' - ) - except Exception: - pass + send_html_email(subject=subj_d, message=email_d, recipient_list=[parcel.carrier.email], title=subj_d) # 4. Notify Admin - notify_admin( - subject=f"Shipment Accepted ({parcel.tracking_number})", - message=f"Driver {driver_name} accepted shipment {parcel.tracking_number} from {shipper_name}.\nPrice: {parcel.price} OMR" - ) + context_admin = { + 'driver_name': driver_name, + 'car_plate_number': car_plate, + 'tracking_number': parcel.tracking_number, + 'shipper_name': shipper_name, + 'price': parcel.price + } + notify_admin_alert('admin_alert_driver_accept', context_admin) def notify_status_change(parcel): - """Notifies parties about general status updates (In Transit, Delivered).""" - msg = f"""Update for shipment {parcel.tracking_number}: -New Status: {parcel.get_status_display()}""" + context = { + 'tracking_number': parcel.tracking_number, + 'status': parcel.get_status_display() + } + subj, email_msg, wa_msg = get_notification_content('shipment_status_update', context) if hasattr(parcel.shipper, 'profile') and parcel.shipper.profile.phone_number: - send_whatsapp_message(parcel.shipper.profile.phone_number, msg) + send_whatsapp_message(parcel.shipper.profile.phone_number, wa_msg) if parcel.shipper.email: - try: - send_html_email( - subject='Shipment Update - ' + parcel.tracking_number, - message=msg, - recipient_list=[parcel.shipper.email], - title='Shipment Update' - ) - except Exception: - pass + send_html_email(subject=subj, message=email_msg, recipient_list=[parcel.shipper.email], title=subj) - send_whatsapp_message(parcel.receiver_phone, msg) + send_whatsapp_message(parcel.receiver_phone, wa_msg)