Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9f5006727 | |||
| 942a39b0eb |
@@ -0,0 +1 @@
|
||||
False
|
||||
@@ -0,0 +1 @@
|
||||
a3dd8e4b-8c69-4536-b080-24506bc95dea
|
||||
@@ -0,0 +1 @@
|
||||
614d490d-41ba-47f9-ae3a-b56948b3d6e4
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTS2/jIBD+Kxbn4GKMMfatbVJtVHUbtdHuam8wjBtUF0cY96Eq/32Fk6rbve0FoREz8714J14/IWnJ+X6/ekYfR7Ig0c0lzrikTFHOtky1jLeszouS84pVv8mCuGt8Iy2pBUABDdISJVJhWUVN2QG1FpuCFZbxSqSZ+mEk7TvRLrf47ADzYfyBYXSDJy356bwdXtJu7XLoh8nmYehx7ceoPSQwy9X99fZ2Q69q+X11cXF8OeKYBuTOkpY0tWClQKQSTE1FozhVNXDKhIamKysrJRzbnI8YvO7z0T5+YrBD9Bih5Tkvc0Z5Qw4LYnXUCbbRI27f9gnJLNMy1Rdzeb627+QZA2n54kPQ47gz6N3Z02gm19uz4xn1+JiY7sOwxxAdzrrMVPteRzf47LTpsnfoI1mQ2/ts0+vYDeHpi1j3R/7r/6C/TdtXrwhTRHs5TD6SliiyIDcads5jtl6SlshCWNEwS0VhNBV111CNpaamko1QprQSk6mXg4/OT8M0Zmsf8SHM8ElLrnQ/4hH4eYCdiwhxConTL5kaN2GwE8TsU/2C5SwvGU9NU9xPMbtD6wJCxMRuG6bTvH9auOSMJShTCOhjttFxl33T4y49sLWtgCPaojQ1Mx1WyqpKQW2lVLIDUQEriwqEgLIzttHGNCVwwTsjsJZkQTCZnc35KpRSHVea1lpzKjgIqroCqdbAFGssGmO/6Jjd9qlPl9YqFIYqkA0VVSmpYYpRLiomDTSVxRSlD0lO3ovkyTUGj/1flG8chGEcupidQpB9EeFu8unvZnMgXpynr7PaywEeMWTJLe18iunJoMPhcPgDAAD//wMANAV6ewIEAAA=
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoyUW2/cNhCF/4rAZ1GmSIq6vMVeGzXcZJ3sJi36xstoTVgmFyTl1DD2vxfUruts0oe+CdTozMczZ/SKnHwCNKAP+/31M7gUUYmSXY4ooQKTDlOyJd1A6EDaqhesJkz8hUpk7+AFDajlWte6B8xAAOaGNFixUWNjoK9JbQhteNaUu4iGVyRtZeDZaqh8/AYhWu/QgP6wzvjvube0lZ78bKrgJ7h1MUmnM8zqenO3Xd/jm1Z8ur68PFZGiFmgsgYNSNW17EwjcF03BvNaU9y3jGEqemWgFbJm5viZdQmCk1MVzeM7g/HJQdIDrSirCKY9OpTIyCQztpIRti/7TLLYtMrn5XK8PA6v6BkCGmj5ZuhR7kJP9uIpqtlO5mLyu511uyvvRrubg0y5cYn2we8hJAuLQetN8c5Uk4pUVFBCUIk+Sv1gHRS3q2I95RtLZkwHXOFOix7zhgmsSEcw5Q0RSveNgQx5B8HB9IPqR6uDj35Mxcn34qzPlXfJutnPsbh1CXYn0AHdyClCLphDAJeKe5keit9kfMikpjWNpgCmZqolaoSmM13T6dYI0YlR80YTVjeac81GZXqpVM805XRUHFqBSrSF8GSdnH73u1328t9+5y9WMMp5Shs/hyUZ0TyiEl1aJ8PLseJrBHOq+nQcxpvSEqhpWi5UnOZ5NVlw6cxfNCBRc8N7koOkJObt2GMJTGLViJ53ihkBOdb3wZtZp59HxghFJfoyu7xKxW0e1nfr8N+C/4T6AxvkXBVLlDWlou17jrUWNeYjUbiDusGCj20vteAUGCrRyutHCEWel7TuTGy9KT4E/WAT6DSHfM0/l97rTXE/yTT68HS2dus57edUfAFjA+gEmWIb5ix1Yyf4Bfbt3icPeXfMTfRvtd8gKB9tyr+Iz7OFbPDmuK6LHf9zW88035n+MxF5I+bk0eFwOPwDAAD//wMA4rnczdgEAAA=
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUyW7jOBD9FYFnUaFEagXmkHYSjJFJ24jdM4O5cSnGRGTSoKikgyD/3qAst51GH+ZGFGt59epVvSPL94A6dH043L6ADQNKUTCTqSBFhUmDC7IlTUeKjrRZTeuqKcl/KEXmHt5Qh2omZS5bwBQqwEyREguqJVYK2pzkihQlizn504C6d8RNpuDFSMjc8Df4wTiLOvSPscq9xtrcZLJ3o8q862Fph8CtjGBubjf329Ua39XV19svX46eAwwxQWYU6pDIc96ossJ5XirMclngtqYUF1UrFNQVz6k6hhkbwFveZ4N6PmNQLlgIsiuygmYEFy36SJHigUfYgg+wfTtEJBNNN9GeTubp2b2jF/CoK9ITocd0V7I3V/tBjKZXV4H7Jwja8z28Ov8ML7xHKTp4dwAfDEz8rDbJGVJOMpIVVUEIStEDlztjIVneJKs+NsypUg0wgRtZtZiVtMKCNAQXrCSVkG2pIGK8B2+hv8j6YKR3g9MhmWlPPtVZOBuMHd04JEsb4MnzcIy74/0AKEXrUfRm2D0CV29b9zjGPzv2sZfF6D3YkKx52CV/8mEXm1C1KmUBoHIqaiI0lI1qykbWqqqaSktWSkLzUjImqRaq5UK0VBas0IJBXaEUrcZwGMOJ/+8XILbe7Pegzgi2E8frngft/H6pwAajTRzNyWMSVd9PXSVzzkVvwIZPJKMOVTlTrCVRTIJjVusWc6Aci7JqWSOoqiBKe+GsNk/jT56I1opRXTZNJVqREyh03TBSa0GU1FS2qhRQFzWpG2haSYgoWd6IVhIFecmiSNfeqVGGX6VASYFS9DjauKHJMrb9aiz+XrEzI2vvgpOujwMzPWzehgD7301t4fYHN5gQ+9fzaL8N8OBs/J1vwMzZHHztwoUzxEVIpt2ThNCa1hoXguWYcU1wW0mGCa1bqrnQTOQoRTdOPoNPosK4sdNQTqJabZJrL3cmgAyjj6X/nbo6zvPutDNnQrKvt9uF83B9OKSz9Y+XNosKXm2SkwA+3ZZvQ/TeuSF2Efx4oaMN9PoE60JNR+Elj6CMBxmmr+0xcObodwq79sFoLsMQ1+AvJydhzEqbXX6tdzGAn9HH4hdDmEUxZ2LNlGe6gJMU/tcB/Pj4+PgBAAD//wMAI6jx6vUFAAA=
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSTS2/VMBCF/0rkdZz6HSc7aIuoKmhFK0Ds/Jj0Wk3tK8cpoOr+d+Tciwrs2EWjmfF3zpm8oGieAI3ozX5/+QyxLKhFJWwlRpjCRGNG7okeCRup6jSTgvb0G2pRuIafaES9cI66ATAHBVh4IrHlk8Pew0AJ9YRJUXeahwWNL8iEzsNzcNCl5TPkJaSIRvQlRJ++17dN6NycVt/lNMNVXIqJrsJcXN5d39/c4ne9+nj59u2xc4GlLuiCRyOylBrtpcKUSo8FdQwPPeeYqcF66JWh3B/HQiyQo5m7xT++MvhUIhQ3so7xjmA2oEOLvCmmYluzwP3PfSXZbLqo9XYrb5/jC3qGjEbW/jb0uO7MzeHMpacnE/3ZFGJYdqhF+5z2kEuAzZJN5TybElJsTo+czwFiQS26uWtuZ1OmlJ/+8unuKP3qP5R/MG4XIjRXF2hEigovBlLbrcGinwZsgBtspRqEttwrqLGdp1hCXNO6NFexwEPeKNGI3pl5gSPfm+x2oYAra67oX1UdhB+hnCdfCxS16DYnv7rSvNpNSUc6TljdsZb9WppP4EMGV6Bqus/raf0/I0wxQirZmjPE0tyasmvem2VXG3zvpWMAnnLbEzuB1F5L7XqvlFaTE9IRTqUTwvHJ+sFYO3DHBJusgF5V7ppusx0UcZJPWmssnOJYSGrxIBjD2knviVVeTZX+1dbmZq5zhnuvQVisnRqwkFxhSzTBTEiirBukh3o7vy05JS40atE15AjzH5I/BJfTkqbSnKJv/jLh0xrrz9psZ/A9RPxjM/8iuUfITQ3PhFjv8pTX4XA4/AIAAP//AwAiBvh48wMAAA==
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTyW7bMBD9FWHOokJSFLXckthBjSB1kBht0RuXcUxEoQyKyoLA/15QdpCmt14IYsB585bhO3j1hNDB+X6/fEYfR8ghurnEKZeENoTTDW06yjsmi4aXrRD8N+TgrvENOqiFMcy0SEqUSISlFdHl1hBrsWWUWcorkTDVwwjdOyhXWHx2Both/IFhdIOHDn46b4eXNFu5wvTDZIsw9LjyY1TeJDKL5f31Zn1Lrmr5fXlxcXw54pgACmehA82YamwlCWOVJYIZTtq6LAmXrbZYS8VKe2xzPmLwqi9G+/jJwQ7RYzQdL3hZUMJbOORgVVSJtlYjbt72icls0yLV87k8X7t3eMYAHc8/DD3CnZnenT2NenK9PTueUY2PSek+DHsM0eHsyyy171V0g89Oky57hz5CDuv77LZXcTuEpy9m3R/1r/5D/iZNX76imSLay2HyEToQHHK4UWbnPGarBXQgmbCipQlIKyLqbUsUloroSrai0aWVmFK9HHx0fhqmMVv5iA9h5g8dXKl+xCPz82B2LqKJU0iifsnUeBsGO5mYfdrPaEGLkiYm6ynup5jdoXUBTcQkbxOmE94/LVxyShOVKQT0MbtVcZd9U+MuPbC1rQxHtKzUNdVbrBrbVI2prZSN3BpRGVqyyghhyq22rdK6LQ0XfKsF1hJywJR2Ni+YsZLVumakaSpORKORtLxGoqjirNZMtiLtxKeP2bpPfaq0tkGhSWNkS0RVSqJpQwkXFZXatJXF1PdhySl80UAO1xg89n9JvnEmDOOwjdlpC7IvJtxNPn3ebN6IF+fJ6+z2YjCPGLKUlnI+7ekpoMPhcPgDAAD//wMA36GF6gMEAAA=
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRUy2rcMBT9FaP1yNHLsuxdmgcNIU1ohrZ0p8d1IuJIgySnDWH+vcgzpc2qdCPE9b1H555z8BsK+hnQiE53u4sXCCWjDSp+LTHCJCYKM7IlaiRspLJVlBCi1He0Qf4aXtGIemEttQNgDhKwcKTDhk8WOwcDJdQR1omKqR8yGt+Q9q2DF2+hjfkLpOxjQCP66oOLP+rb2rd2jotrU5zhKuSig61kzi/ur7e3d/iyl58uPnw4dGbIFaD1Do3IUKqV6ySmtHNYUMvw0HOOmRyMg15qyt1hzIcCKei5ze7pDwcXS4BiR9Yy3hLMBrTfIKeLrrSNzrB93VUmq0zntb5Zy+t1fEMvkNDINr8FPcCd2NmfPGez+NmdHM6i81Nez0nbEtMr2qBdijtIxcOq0WnO8Gzm163OT5eHnnrNFz/BLgXcWVxCQSPiEm3QqtE86+JjaI4Uz2YPoawfS/Ihe/svqIp0e9/czbpMMT2/s+T+oPLVf4h8o+2jD9BcnVdsKpwYSG03Got+GrAGrrHp5CCU4U5CTchZDMWHJS65uQoFHtK6EhrRpZ4zHPidJvvoC9iypLrnN1kH71J0iy3NHyspaUnLCatDS9ktpfkMziewBeoS27Qc8b68H2GSEVKpLClBKM2dLo/NR50fa4PrXWcZgKPc9MRM0CmnOmV7J6WSkxWdJZx2VgjLJ+MGbczALRNsMgL6KjDU5DRrWEnHeqGowsYMBAtKCB5Iz3FvFBAhmenNex2b27nOae6cAmGwsnLAouMSG6IIZqIj0tihc1Bz+VuSYx6EQht0DSnA/NfKN96mmONUmqPXzTsRPi+h/gia1fcfPuCfq9rn0T5Baqpb2oea+aNB+/1+/wsAAP//AwA/mN8hTwQAAA==
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUzW7dNhCFX0XgWpT5L1K7JHZQ10htxEZTdEdyRrYQXeqCopwGxn33gvIN3HSXHTHgGZ75eMgXkvwByUDeHY9Xz5jKSlpSpr0kmDCUWSrYA7MDE4Owneq16nn/N2nJdIPfyUB6FSOPDqlEg1QB0zTIMVIAdJxxYEKr2tM/rmR4IX7qAJ+niN2y/ol5nZZEBvJlSrB8q2f7qYvzskGXlxmv01p8itXM5dX9zcPtHf3Ymz+u3r9/3bniWht0E5CBGBCSGeQUmBFUKbTUgkFqDSgWOdcC3KtsSgVz8nO3wtc3D7CUhCUOohOyY1Q4cmoJ+OKr7eBXfPh+rE52TJe13u7lfTm8kGfMZBDtD6Cv7S7iPF2U5TjjM85Hn1fMF3E5HHwC0pJjXo6Yy4Q7mn3aefZlWlJzPuzDPGEqpCW3983d7Mu45MNPvO5fEVz/AoFnzIEMRHoFQSFy1C4i98zz4EZtAhjQmhmwUTCFYuSgGQ+9l14xwdFIo9FCIC15nJfg59/XnZ8TLFhgvTACFTqhjHGRobQ46h4ZeGOCsdGBtL1QFqz0znvuAvbRmVhD8snHpylhc31Zp+EKlGNAFQ+eqn501KP0NGjjlA0SDFbNhyWVKW3LtjbXqeBj3vmRgXz084qv5N7l+DQVjGXLFepfpgrv8gJbLM1bAjjrWCeZqKKtHLfSfEaYMsaCFe9D3s79/icRRjBWrWw5YyrNnS9PzW9+faoboAcdBSJwGXoWRtQWrLaxB2OsGaPSkUmuo1JRjgGcD8HJKJQYg8LekJZgDVyzZ1yMVpjYR9rzqKnSY6ROiJ5G5ZkPzslRVSNvHJvbueq8BLCoArXROKq0NDQwy6hQmpkQnQascf6B5Bw+ZUlLbjAnnP8z8qcp5mVdxtKcU9j8BOHzlur/0eyJ/DYl+s9O+3KJXzE39bb8lOpTOV/QqSUH9OuW8bB/P8MLuavvpHnYfyHOVWeYasl98blsx3PZik5o6U6n0+lfAAAA//8DAFrkfDnDBAAA
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUXU/dOBCG/0rk6zh1HNtxcteeA1qECghQt9o7xzMuVoN95Dh0EeK/r5xzaJfe9S4Zzcc7z7zJCwnmEclIPh4OZ08Y8kJqkv0W4owryjTl7J7pkfGRD83Ateql+IfUxF/iMxlJL6xt7YC0Q4VUAJN06pylADi0rAXGpSg9zbeFjC/E+AbwyVts4vIF0+JjICP52weIP8ps4xs7xxWaFGe8CEs2wRYx+7O7y/vrG3req6uzT5+OmQsupUHjgYxEAe+YwpYCU5wKgZpqUEi1AsFs20oOw7HMh4wpmLlZ4PsvDRBzwGxH3vCuYZQP5LUmYLIpsiez4P3zoSjZMO1LvN7C2+P4Qp4wkZHXb0CP7T7Y2X+w8fHRBEi4xHnNPoZ3kScEUpNDigdM2eNGaVt8nk1Jrk5zd7PHkElNru+qm9lkF9PjO3SnplfH+RLA9b3g6CahcEIG2DPX6W5iptWub1sjmXOqFziBYL2VAx80ain7fuhFQXV3xHvxB3RPGm6PiyUyks/eprhEl5t9zFeYm93sm90x7dzYHNPz2+vtT0DNjckPu9961eSzsQ8+YHWxL4paAWJgQEU7GSp6N1CDnaGTVIPQUwcKi/F2MWQf1rgu1UXI+C1tUMlIzs284BHnx2QffEab11TQfVWl8CZFWG2ufjmkZQ1rOsZL0ZoPa65uEXxCm7Eguk/rqd9vJVxxxoqUNSUMuSrLVX+Z5aEkQA/SckRou6lnk0OpQUtte1BKK2eFtKxrpRXCdm6CwUzT0FkuuJsE9orUBIshq+0bMI5jy6WjDpyjwihOtW0N5S1zApwUvHXvOFbX81bXAWgUE9VWDVTITtGJaUa5kExNdpCAxe5vSE6OFJrU5BJTwPl/K/88eHWyZvUOwu0ayv+l2lz1wwf670Z7H+13TFW5lvFhc87xQK+vr6//AQAA//8DAME6mqamBAAA
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUUW/cKBSF/4rFs3EwYAx+a5tUm426GTWj7WrfLnCdoHrwCOPpVtH89xWeqbLdt76hK869h48DryTCAclA3h2PdyeMeSE1yWErccYVZZpytmd6YHwQrFFScsP136Qm4QG/k4H00rnWGaQCFVLpWUetGB31Hk3LWs94J0tPeF7I8EogNB5PwWEzL39iWsIcyUC+hOjnb2U2hMZN8+qbNE94H5cM0RUzt3dPD/vHHf3Yqz/u3r+/7FxwKQ2a4MlARj0q3VpO21GOVI5aU809o71BPhrhWc/1RRZixhRhahb/9c2Dn3PE7AbecNEwyg0518RDhmLbwoL778fiZMN0W+r1Vt6Wwys5YSIDr38AvbS7cVO4yfNxwhNOR0gLphs3Hw4QPanJMc1HTDnghmY77TRBDnOsrsM+TAFjJjV5fKp2E+RxToefeD1dENz/AoETJksG0ukORNsjSK/R+FF1yE0ndMtQShA99wzGUSrhvGJGjQjcO+lc33LujVCkJs/TbGH6fdn4Gc6sLjMUR4mGS6WMYyg0jl2PzINSVmlnvNA9l9prAQagNRZ7Z5QrIfkE7iVErO5vyUBUK700zFPZWqCyHw0FFEBtp4zUVniFRfNhjjnEdV6X6j5mfE4bPzKQjzAteCH3LrmXkNHlNRWof6ki3KXZry5XbwloWcMawXgRrfm45uoz+pDQZSx492m99vufhCvOWLGypoQxVzvIL9VvsLyUDb73neOIvhW2Z3bETnvdadd7pbQanewcE23npHRitN6AtUY4LvloJfaFMpbAVVvGpWHGcqepEYZRyTRQDb2hXScQmDFC+uL+jWP1OBUdCO81Sku1U4bKTihqmWaUy44p60znscT5B5Jr+GRJywOmiNN/jvwpuDQv85irawqrnyB8XmP5P6otkd9CpP9stG9n9xVTVW4LQixP5XpB55ocEJY14WH7foZXsivvpNpvv5BRjVBC1+QpQ8rr8VpuedvwlrXn8/n8LwAAAP//AwAW3FooxAQAAA==
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoyTT2/bOBDFv4rAs6hQFEVRujVxgg3S1kHt3S72xj8jh4hCGiSVNgj83QvKTlu3PezNoEdvfvPmzSty8gnQgN7t99fP4FJEJUp2eaKEckwEpmRLxEDo0JBKNIxxIf5DJbJ38IIG1DGta90DboADZoa0WDWjxsZAX5PaENqyrCl3EQ2vSNrKwLPVUPn4D4RovUMD+myd8V9yb2krPfnZVMFPcOtikk5nmNX15m67vsc3Hf94fXl5rIwQs0BlDRrQKEYuakVxPbIRs1EILKghuOuBjn1jSEfF8TPrEgQnpyqaxx8MxicHSQ+0ok1FMO3RoURGJpmxlYywfdlnksWmVX4vl+fl5/CKniGggZZvhh7lLvRkL56imu1kLia/21m3u/JutLs5yJQbl2gf/B5CsrAYtN4UP5hqUpGKckoIKtEHqR+sg+J2VaynPLFsjBHAFBaa95i1DceKCIIpawlXum8NZMg7CA6mn1Q/WB189GMqTr4XZ32uvEvWzX6Oxa1LsDuBDuhGThFywRwCuFTcy/RQ/CXjQyY1nWk1BTB1ozqiRmiFEa3QneFc8FGzVpOmbjVjuhmV6aVSfaMpo6Ni0HFUoi2EJ+vk9N7vdtnL7/3O/1jBKOcpbfwclmRE84hKdGmdDC/Hir8jmFPVx+My3pSWQE3TMlBx2ufVZMGlM3/RgHjNDOuJwaxWErNu7LGERmLV8p4J1RgOOdb3wZtZp19X1hCKSvRpdvmUitu8rC/W4a+c/YL6ExvkXBVLlNu6q7VmFEtGCWaUSNwzSjEXtRTQEMlUjvLK60cIRd6XtO5MbL0p3gX9YBPoNIc85r9L7/WmuJ9kGn14Oju79Zz2cyo+gbEBdIJMsQ1zlrqxE/wG+zb3yUOWcTbHa1ym/Z/HeOVd9H+Q/+PGc+Ln5NHhcDh8AwAA//8DAKH1mMy4BAAA
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUS2/dNhCF/4rAtSjz/dAuiR3UMFIbsZEW3VHkMCaiS11QlNPAuP+9oO4NUnfXnTDgGZ355pCvKLsDoBG9Ox5vXiDXFfWopr3ECFOYGMzIEzEjYSMngyaMc0n+Qj1Kd/ADjUgL76m3gDkowCIQiScePQ4BLCU0ECZF6+m+rmh8RS4NAV6Sh2FZv0BZ05LRiP5IOSzf279dGvy8bGEoywy3ea0u+2bm+ubx7un+AX/U6veb9+/PJ1dYW4MhBTSiaKIydGKYRhGxiMZgwwLB2gKLlgeimTnLUq5QspuHNXz75SEsNUP1IxsYHwhmFp16FFx1zfbkVnj6cWxOdkzXrd7v5f1zfEUvUNDI+p9Az+2u/Jyu1m2a4QXmoysrlCu/HA4uB9SjY1mOUGqCHc0LlGlZU21UvdfOGEqDcVpIHQyXjOvACQRDtAw2WKeEisxLGcNkrBZRBEZFUCDEpFGPdnjz7Gpacnfx/mFOkCvq0f1j9zC7GpdyeIP/8Uz09n8AbbbRiKSRjlMNTgQDNkQlgVnJDSUghOOaBeJiFIr7oIhVERwLXnivKWPBcoV69Mn555Shu71GI1JUBGFJwIJODgsdLXbAHZ6kssJMvA2KevRhyTXlbdnW7jZX+Fr2edGIPrp5hfOk74p/ThV83UqD8KdqwoeyhM3X7lcAKBnIwAlroq0et9p9hpAK+AoNx1PZLv3+I2GKEdKsbKVArt2Dq8/db259bgeCDtIzgED5pMkUQZpgpPE6KGVU9EJ6wqn0Qngep2DdNFnumWBxEqAbFWh5684Rt0IyFSV2QAUWlig8Ea8x8UIpb8ATbt9w7O7npnM8BANiwsYri4XkTWcIZkISNXkrA7Q0/0RyCYto272DkmH+18ifki/LusTaXVLTvYHwecvt+ej2BH1PGf+9075e/DcoXduWS7ndlMuCTj06gFu3Aof99Rlf0UO7Jt3T/ghZNXDFTY8eqyt1O17KlNGBUUJPp9PpHwAAAP//AwACiQ5HwwQAAA==
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTS0/jMBD+K5HPdfArjpMb0KKtEEsF1e5qb35MqEVwKsfhIdT/vnJaxLK3vVjRxDPzvfyOgn4C1KLz/X71DCGNaIGSn0uMMImJwoxsiWoJazkt65qwmtHfaIH8NbyhFtXCWmobwBwkYOFIhQ3vLHYOGkqoI6wSeaZ+GFH7jrQvHTx7C+Uw/oA4+iGgFv30wQ0vebf2pe2HyZVx6GEdxqSDzWCWq/vr7e0GX9Xy++ri4nhzhDEPKL1DLepUJxU1DNNOdFh0SmHFHMF1A6xruCM1U8c2HxLEoPtydI+fGNyQAiTbspLxkmDWoMMCOZ10hm30CNu3fUYyy7TM9cVcnj/bd/QMEbVs8SHocdyZ7f3Z02gm37uz45n0+JiZ7uOwh5g8zLrMVPteJz+E4rTpsvcQElqg2/ti0+vUDfHpi1j3R/7r/6C/zdtXr2CnBO5ymEJCLco/brTd+QDFeolaJKlwoiEOC2o0FnXXYA1cY1PJRijDnYRs6uUQkg/TMI3FOiR4iDN81KIr3Y9wBH4e7c4nsGmKmdMvmRs3cXCTTcWn+pSUpOSE5aYp7adU3IHzEWyCzG4bp9O8f1qYZIRkKFOMEFKx0WlXfNPjLl9wtassA3CUm5qYDirlVKVs7aRUsrOisoTTygpheWdco41puGWCdUZALdECQTa7mPPlKmC04h2GmtdYOMqx0VWHK9m4ChSlBugXHYvbPvdp7pwCYbCyssGi4hIboghmoiLS2KZykKP0IcnJe5E9uYYYoP+L8o23cRiHLhWnEBRfRLibQn67xRyIFx/w66z2crCPEIvslvYhx/Rk0OFwOPwBAAD//wMAz5D+vAIEAAA=
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRV227cNhD9FYGvlRTeJFEC8uCs16kR+ALvoin6xsvQy1omFyTl1DX23wtp7WadoA990WXImTlz5gz5grx8BDSgs/1+/QQ+J1Si7BYTxbStsKgo3mIxYDowUncC44azP1CJ3Bd4RgPquNZE91AxaKHiBjeVYlZXxkBPMDGYNnyOKe8TGl6QdLWBJ6ehDuk3iMkFjwb01XkTvs25pav1GCZTxzDCpU9Zej2DOV9vvmxvbquLrr1ef/p03JkgzQFqZ9CArLCtIIpWxHJbcStEJajBVdcDtT0zuKPi6OZ8hujlWCfz8B2DCdlD1gOtKatxRXt0KJGRWc6wlUywfd7PSBaazmd7uZiXz+EFPUFEAy3fCD2G+6BH9+ExqcmN5sPyRCXax7CHmB0sjCxFjqPMLvjiNcdqdOAzKtGn2WUzaQ0poQFdyDHBm3nt752Hiygf4VuID9fHvPX1eotKdBvDn6Dzrcw7NCBmtTBWaSJ5r6wC07e4kdBLyxUX1hhGtNWUWNx2naLcMmGUkm3bNUYpSlGJbjbF7SizDfHxXcM2xx5c/o8WbM5Way/VCObHkrYy3kNGA1KEWC26vueGNr1hILUATlplsBSCAQfDur63RDBGmSBWYCV62QisGuCGoRJdpk2W3sgxeFj/BXqaCUYD2sZpzncl9c55KC7P0YBawg3vsak4UbLine0rCUxWqml7LhQzLcwivpBunCKsZIb7EGf1X0+fYe7TKvjs/BSmVFz6DPdxaedJeTeb4izqncug8xTnRv3eziFvYzCTzsV3HRJc45rhhfMp76dc3IFxEXRe+HqFf7P50YW2FOMZyhQj+FzMrS9+lWnuPzGdaTQFMISpDisLjTCiEbozbStaq3mjMSON5lwzq0wvleqZppxaxaFrUYlgln2xTBontLWNtZViXVNxqk0lW6MrQbGGtmecA33HcHEzzn6SGSOAq0rotq94w9pKYYEryhvcKt03BuR7cZ+UKOq2JqRmDOP+hLfXgeGzrtYxhrgK03yKDad/l96G4qVYhce9GyEWH4uyuEpLns/gIcrx1LR+kuN0HMdT65uE3nlHud8thq1MD8vHxjzcQQrjE8S0GK4hb8xxbVFL8bGgZbG4r3agX1dkdk+wDWHUO+mOOVbBwJmX43Nyx0h38u9wBP91b5f3WdpfLxHL4ibvlsqKAyrRF4gexhOFXDkdQwo2F6+TW7zTzAnl5y7tR/n8E/O/sEY1PVOg3vYv6H8e5LvJzxdIsZwI35yv/lqEfh70A8RiHhTp/HxW/utxNY3ZbXcRpAFzFQz8GPRQokeQaYrwuFxRy6npIS4wzqfjsF36KzeOLoEO3iQ09O0Mm/NXtP+1jRAiaoKFOBwOh38AAAD//wMAez5l7Q8HAAA=
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSTTW/cIBCG/4rFeXEwYIx9S/OhRlGaqFm1VW98DA2KAyvASaNo/3uFd6s2vfWC0GhmeOd5hzcU1BOgCZ3udhfPEEpGG1T8GqKECkwkpmRL5EToxLp2EF0vB/YdbZC/hlc0oYEb05kRMAMBmFvSY82cwdbC2JHOEtrz2lP9yGh6Q8q3Fp69gTbmL5CyjwFN6KsPNr7Ut5VvzRwX26Y4w1XIRQVTxZxf3F9vb+/w5SA+XXz4cMjMkGuD1ls0ISedkJ2muHPcYe6kxJJagocRqBuZJQOVhzIfCqSg5jbbxz8abCwBiploS1lLMB3RfoOsKqrK1irD9nVXlayYzmt8s4bX6/SGniGhiW5+Az20OzGzP3nKevGzPTmcReXHvJ5OmRLTK9qgXYo7SMXDyug0Z3jS8+tW5cfLQ0695oufYJYC9iwuoaAJ1XFWRPOsio+hOSo8mz2Egjbo9r65m1VxMT29g3x/4Hb1H9hulHnwAZqrczQh0XHLR2Ix77TCfHAjVsAU1r0YudTMCqien8VQfFjikpurUOBHWlWiCV2qOcNB32kyD76AKUuq0r+JWniXol1Maf6Y05GWtIzQWrSU3VKaz2B9AlOgDrFNy7HfPyVUUEKqlCUlCKW5U+Wh+ajyQ02wg+0NBbAd0wPRDnppZS/NYIWQwhneG8K63nBumNN2VFqPzFBOneYwCLRBUHehWddPO6p6xwbcEWswF85g1VGCpVOiBys1Z8M7js3tXOsUs1YC11gaMWLeM4E1kQRT3hOhzdhbqJv2G8nRYl49uYYUYP5r5BtvUszRlebodfMOwucl1K/drL6/+IB/rrTPo3mE1FS3lA91i48G7ff7/S8AAAD//wMAGYtnLCEEAAA=
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTS2/cIBD+KxbnxQGMMfatTVI1itpESdRWvWFmyKI4sMI4D0X73yu8W7XprTc0mvnmewxvJJhHJAP5sNudP2HIM9mQ7NeSYEJRpqlgd0wPTAwNrzvNOeftT7Ih/hJfyUA6aS23PdIGFVIJrKVj4ywFwJ4zDky0smCa+5kMb8T4GvDJW6zj/A3T7GMgA/nuA8Tnstv42k5xgTrFCS/CnE2whczZ+e3l3dU1/dSpr+cfPx46Z5wLQO2BDMRppzQfBeVOOiqd1lQLYLTrUbi+AdYJfRjzIWMKZqpnePjDAWIOmO0gatHUjIqe7DcETDaF9mhmvHvdFSarTWelvlnL63N4I0+YyCA2vw09wJ3YyZ/Y+PhoApw4H/y8JRuyS3GHKXtcLVlVTpPJPobquOR08hgy2ZCr2+p6MtnF9PjOp9uD9Iv/UP7F2K0PWF2ckYEoLkH2DKjko6Gycz012Bg6tqqXemxAYYntNIbswxKXuboIGe/TypIM5JOZZjzw+5Ds1me0eUmF+g9VBvHF59MIpcDJhlynCIvN1R+7OatZ3TBRMJa8W3J1g+AT2oxF011ajvD/jAglGCvMlpQw5Ora5G312czb0gAdtFYgAm/Gjo0OWw261bYDpbRyVraWNby1UtrGjdCbcewbK6Rwo8ROFd4l3Wo9KN5oyXXHqbXgqIQeae9YSyU40K2UQoF7Z2t1NZU50wBolCPVVvVUto2iI9OMCtkyNdq+BSy389uSY+KyRHSJKeD0l+Qv3qY4R5erY/TVOxNullA+a7WewbMP9GU1/yzaB0xVCc/4UO7ymNd+v9//AgAA//8DAFu9oLHzAwAA
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyhT/KZuSeyghpHaiBdp0Rs/hjERLbWgKKeBsf+9oLyBm956I4ac4TsPX84LyvYIaELvTqebZ8h1RT2qaQ9RQiUmGlNyIHoidGJ0oJwpQfhfqEfpDn6gCSnu/egNYAYSMA9EYMeixyGAGckYCBW81bRfVzS9IJuGAM/Jw7CsX6CsacloQn+kHJbv7W6bBj8vWxjKMsNtXqvNvom5vnm8O9w/4I9K/n7z/v3ryRXWVmBIAU0oaiV15Ap7KTTmnglseTRYK0lFZFYANa9pKVco2c7DGr69aQhLzVD9RAfKBoKpQeceBVttk+3sCocfp6Zkx3Td4v0e3pfTC3qGgiba/wT6Wu7Kz+lq3dwMzzCfbFmhXPnleLQ5oB6dynKCUhPsaJ6huGVNtVH1XlmtxzFoq7hQQTNBmQqMQNBEiWCCsZLLSL0QMThtFI880JEHCZw7hXq0w5tnW9OSu4v2D3OCXFGP7h+7h9nWuJTjL/gfX4ne/g+gTTaakNDCslGB5UGDCVG2bcH0SIBzyxQNxMbIJfNBEiMjWBo8916NlAbDJOrRJ+ufUobu9hpNSI48cEMC5qOzmKtosAVmsRPScO1YaxT16MOSa8rbsq3dba7wtez9ogl9tPMKr52+K/4pVfB1Kw3Cn7IlPpQlbL52bwYYyUAGRmhL2uppq91nCKmAr9BwHMp2qfefFCopIU3KVgrk2j3Y+tT9ZtendiCoIDwFCCNzirgIQgcttFdBSi2j58ITNgrPuWfRBWOdM8xTTqPjoBoVaH7rdosHDcyNRGNposXceIeNNA7T6AUnI2hhm63eOHb3c8uzLAQN3GHtpcFcMIkd0QRTLoh03ogAzc0/kVzMwjXq0R2UDPO/Wv6UfFnWJdbu4pruFwift9zGR7c76HvK+O+d9vXiv0Hp2mvZlNtPuTzQuUdHsOtW4LhPn+kFPbRv0h32ISTUIAjTPXqsttTtdAlLMQhB2Pl8Pv8DAAD//wMAUcbLucIEAAA=
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUS2/cNhSF/4rAtShTfFO7JHZQ10g9iAdN0R0fVzYRDTWgqEkDY/57Qc0EqbvLTrjiuTz8eHhfUbIHQAN6dzzenSCVBbWoxK1ECZWYaEzJnuiB0IHRrpdaCS3/Ri2KD/AdDUhx73tvADOQgHkgAjs2ehwCmJ70gVDBa0/7vKDhFdnYBThFD928/Al5iXNCA/oSU5i/1b1t7Pw0r6HL8wT3aSk2+Wrm9u7pYf+4wx+V/OPu/fvLygWW2qCLAQ1o1ErqkSvspdCYeyaw5aPBWkkqRmYFUHORxVQgJzt1S/j600OYS4LiB9pR1hFMDTq3KNhiq21nF9h/P1YnG6bbWm+38vY5vKITZDTQ9gfQS7sbP8WbMh8nOMF0tHmBfOPnw8GmgFp0zPMRcomwodlOO022xDk1180+TBFSQS16fGp2ky3jnA9veD1dENz/AoETZIcGJLSwrFdgedBgwijrb8F0T4BzyxQNxI4jl8wHSYwcwdLgufeqpzQYJlGLnqfZ2en3ZeNnKHE6EEUlBQ6GcimNJ8A0jEIBCVZKJ7U3gWlFuQ6aWWNtbxwob6SvIflk/UtM0NzfogHJngduSMC8dxZzNRpsgVnshDRcOxYkVM2HOZWY1nldmvtU4Dlv/NCAPtppgQu5d9m/xAK+rLlC/UtW4S7PYfWl+ZmAnnSkY4RW0VqOa2k+Q4gZfIGKd5/Xa7//SaikhFQra86QSrOz5aX5zS4vdUFQQXgKEHrmFHEjCB200F4FKbUcPReesF54zj0bXTDWOcM85XR0HFSlDDVwzZZxanrDKCHYjJxibpzERjuFGVHeh970AtQbjs3jVHWWhaCBO6y9NJgLJrEjmmDKBZHOGxGgxvkHkmv4uEYteoCcYPrPkT9Fn+dlHktzTWHzBsLnNdX50WyJ/BYT/mejfTv7r5Cbels2pvpUrhd0btEB7LJmOGzjZ3hFu/pOmv02hYTqBGG6RU/F5rIer2UpOiEIO5/P538BAAD//wMARF4PZcMEAAA=
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTy07cMBT9lcjrODi24zjZwQyoCFEQjNqqOz9uGItgjxyHh9D8e+XMIEp33VlXPsfncf2OvHoC1KPT3e78GXyaUImSW0aUUIGJxJRsiOwJ7Rmrmo4TIdrfqETuCt5Qj1puTG06wAwEYG5JgzUbDLYWuprUltCGZ071MKH+HSlXWXh2Bqow/YA4ueBRj346b8NLflu5yoxhtlUMI1z6KSlvspj1+f3V5uYWX7Ti+/nZ2eHmBFMmqJxFPRpkK+TAW2xEIzE3rMGKDx2WraDNwFQDtDvAnE8QvRqryT5+arAheUimpxVlFcG0Q/sSWZVUlq3VBJu3XVayxLTO83IZL8f+HT1DRD0tPwI90J2Y0Z2Y8PSkvD0ZnHfTFpVoF8MOYnKwRLK4HEeVXPDF8ZHV6MAnVKKb++J2VGkI8elLTvcH65f/4fxama3zUFyuUY9EzS3viMW81grzduiwAqawbkTHpWZWQK5tFXxyfg7zVFz6BA9xUYl6dKHGCQ76TqPZugQmzTFL/yUyEF5dWgWbBwSV6DYGO5tUfMZdk4pUjNDMMafdnIo7sC6CSZA9beJ8pP8HQgUlmXE1xwg+FbcqbYtvatrmC7a1jaEAtma6JXqARlrZSNNaIaQYDG8MYXVjODds0LZTWnfMUE4HzaEVWXdut1gWCqxqKa8VZtBpzBlrsWZCYU25YhIoJZB34DPW4mbMOMWslcA1lkZ0mDdMYE0kwZQ3RGjTNXbBfURybJxLVKIriB7GvyxfOxPDFIZUHKsvvoRwN/v8WYtlDV6cx69L+OtgHiEWuTzlfN7LY1/7/X7/BwAA//8DAEtP7wnzAwAA
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTTW/cIBD9KxZn42DAGPvWZlM1itpEzaqtesPMOIvihRXGaato/3uFd6M0vfWGRrzH+xieiTd7JD15dzhcPaFPMylJcuuIM64o05SzLdM9470QVdsIxYT+QUribvA36Ukrra1th1SgQiqBNXQQo6UA2NWsBsYbmTnNw0z6Z2JcBfjkLFZh/opxdsGTnnxzHsLP/LZxlZ3CAlUME177ORlvs5jN1f3N9vaOfmjV56v37083Z5wzQeWA9EQBF0xhTYEpTqVETTUopFqBZLauGw7dCeZ8wujNVM3w+KoBQvKYbM8rLipGeUeOJQGTTJY9mBm3vw9ZyRrTJs/Ldbwe+2fyhJH0vHwJ9ER3YSd3YcN+bzxcjM67eUdKcojhgDE5XCNZXU6TSS744vzI5eTQJ1KS2/vibjJpDHH/Jqf7k/Xr/3D+ydid81hcbzKmliA7BlTWg6GyHTtqUBg6NKqTehCgMNd2GXxyfgnLXFz7hA9xVUl68sFMM570vYt25xLatMQs/bvKQPzl0mWAPKhJSe5igMWm4jXumlWsEoxnjiUdllR8QXARbcLsaRuXM/0/EK44Y1nZEiP6VNyZtCs+mnmXL0ALjeWIUIuhZcOIjQbdaNuCUlqNVjaWibqxUloxDtCZYeiE5ZKPg8RWZd253WJdKItqHBvZ0tpoSSWIjupOaQq1sFZzrqXJQl5jLW6njDMCQKMcqLaqo7IRig5MM8plw9RguwYw785LJOfGpSYlucHocfrL8idnY5jDmIpz9cWbEL4sPn/WYl2Dn87TX2v4m2AfMRa5PON83stzX8fj8fgHAAD//wMAJODDNvMDAAA=
|
||||
@@ -33,6 +33,9 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
||||
public DbSet<PriceList> PriceLists { get; set; }
|
||||
public DbSet<PriceListItem> PriceListItems { get; set; }
|
||||
public DbSet<Supplier> Suppliers { get; set; }
|
||||
public DbSet<DriverProfile> DriverProfiles { get; set; }
|
||||
public DbSet<DriverComplianceConfig> DriverComplianceConfigs { get; set; }
|
||||
public DbSet<RideCancelRequest> RideCancelRequests { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -65,6 +68,28 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
||||
.HasIndex(pli => new { pli.FK_PriceListId, pli.FK_ItemId })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<DriverProfile>()
|
||||
.HasIndex(dp => dp.FK_UserId)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<Vehicle>()
|
||||
.HasOne(v => v.DefaultWarehouse)
|
||||
.WithMany()
|
||||
.HasForeignKey(v => v.FK_DefaultWarehouseId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<DeliveryRun>()
|
||||
.HasOne(r => r.SourceWarehouse)
|
||||
.WithMany()
|
||||
.HasForeignKey(r => r.FK_SourceWarehouseId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<DeliveryRun>()
|
||||
.HasOne(r => r.DriverProfile)
|
||||
.WithMany()
|
||||
.HasForeignKey(r => r.FK_DriverProfileId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
Seed.RoleAndPermissionSeed.Apply(modelBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,19 @@ public static class RoleAndPermissionSeed
|
||||
("categories.create", "Create categories and subcategories"),
|
||||
("categories.update", "Update categories and subcategories"),
|
||||
("categories.delete", "Delete categories and subcategories"),
|
||||
|
||||
("driver_profiles.view", "View driver profiles"),
|
||||
("driver_profiles.create", "Create driver profiles"),
|
||||
("driver_profiles.update", "Update driver profiles"),
|
||||
("driver_profiles.deactivate", "Deactivate driver profiles"),
|
||||
|
||||
("compliance.manage", "Manage driver compliance configuration"),
|
||||
|
||||
("rides.view", "View rides"),
|
||||
("rides.create", "Create rides"),
|
||||
("rides.manage", "Manage ride lifecycle"),
|
||||
("rides.cancel.request", "Request ride cancellation"),
|
||||
("rides.cancel.approve", "Approve or reject ride cancellations"),
|
||||
];
|
||||
|
||||
private static readonly Dictionary<string, Guid> PermissionIds = PermissionDefs
|
||||
@@ -100,9 +113,16 @@ public static class RoleAndPermissionSeed
|
||||
var itemPerms = new[] { "items.view", "items.create", "items.update", "items.delete", "categories.view", "categories.create", "categories.update", "categories.delete" };
|
||||
|
||||
var salesRepPerms = new[] { "orders.view", "orders.create", "orders.update", "stores.view", "routes.view" }.Concat(itemViewPerms).ToArray();
|
||||
var supervisorPerms = salesRepPerms.Concat(["orders.delete", "stores.create", "stores.update", "stores.credit.update"]).ToArray();
|
||||
var areaManagerPerms = supervisorPerms.Concat(["vehicles.view", "warehouses.view", "stores.kyc.verify"]).Concat(itemManagePerms).Distinct().ToArray();
|
||||
var areaSupervisorPerms = areaManagerPerms.Concat(["routes.create", "routes.update", "stores.delete"]).Concat(itemPerms).Distinct().ToArray();
|
||||
var supervisorPerms = salesRepPerms.Concat(["orders.delete", "stores.create", "stores.update"]).ToArray();
|
||||
var ridePerms = new[]
|
||||
{
|
||||
"driver_profiles.view", "driver_profiles.create", "driver_profiles.update", "driver_profiles.deactivate",
|
||||
"compliance.manage",
|
||||
"rides.view", "rides.create", "rides.manage", "rides.cancel.request", "rides.cancel.approve"
|
||||
};
|
||||
|
||||
var areaManagerPerms = supervisorPerms.Concat(["vehicles.view", "warehouses.view"]).Concat(itemManagePerms).Concat(ridePerms).Distinct().ToArray();
|
||||
var areaSupervisorPerms = areaManagerPerms.Concat(["routes.create", "routes.update"]).Concat(itemPerms).Distinct().ToArray();
|
||||
|
||||
var regionalManagerPerms = new[]
|
||||
{
|
||||
@@ -112,6 +132,9 @@ public static class RoleAndPermissionSeed
|
||||
"vehicles.view", "vehicles.create", "vehicles.update", "vehicles.delete",
|
||||
"warehouses.view", "warehouses.create", "warehouses.update", "warehouses.delete",
|
||||
"regions.view", "users.view",
|
||||
"driver_profiles.view", "driver_profiles.create", "driver_profiles.update", "driver_profiles.deactivate",
|
||||
"compliance.manage",
|
||||
"rides.view", "rides.create", "rides.manage", "rides.cancel.request", "rides.cancel.approve",
|
||||
"items.view", "items.create", "items.update", "items.delete",
|
||||
"categories.view", "categories.create", "categories.update", "categories.delete"
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Distribution.Infra.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.DriverComplianceConfig;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/driver-compliance-config")]
|
||||
[Authorize]
|
||||
public class DriverComplianceConfigController(DriverComplianceConfigService service, ICurrentUserService currentUser) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Authorize(Policy = "compliance.manage")]
|
||||
public async Task<IActionResult> Get()
|
||||
{
|
||||
var result = await service.GetAsync();
|
||||
return result is null ? NoContent() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Authorize(Policy = "compliance.manage")]
|
||||
public async Task<IActionResult> Upsert(UpdateDriverComplianceConfigRequest request, CancellationToken ct)
|
||||
{
|
||||
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||
var result = await service.UpsertAsync(request, userId, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Distribution.Features.DriverComplianceConfig;
|
||||
|
||||
public record DriverComplianceConfigResponse(
|
||||
Guid Id,
|
||||
bool EnforceLicenseExpiry,
|
||||
bool EnforceMedicalClearance,
|
||||
DateTime UpdatedAt,
|
||||
Guid UpdatedBy);
|
||||
|
||||
public record UpdateDriverComplianceConfigRequest(
|
||||
bool EnforceLicenseExpiry,
|
||||
bool EnforceMedicalClearance);
|
||||
@@ -0,0 +1,16 @@
|
||||
using Distribution.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.DriverComplianceConfig;
|
||||
|
||||
public class DriverComplianceConfigRepository(AppDBContext context) : IDriverComplianceConfigRepository
|
||||
{
|
||||
public async Task<Models.DriverComplianceConfig?> GetAsync() =>
|
||||
await context.DriverComplianceConfigs.FirstOrDefaultAsync();
|
||||
|
||||
public async Task AddAsync(Models.DriverComplianceConfig config) =>
|
||||
await context.DriverComplianceConfigs.AddAsync(config);
|
||||
|
||||
public void Update(Models.DriverComplianceConfig config) =>
|
||||
context.DriverComplianceConfigs.Update(config);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Distribution.Infra.UoW;
|
||||
|
||||
namespace Distribution.Features.DriverComplianceConfig;
|
||||
|
||||
public class DriverComplianceConfigService(IDriverComplianceConfigRepository repo, IUnitOfWork uow)
|
||||
{
|
||||
public async Task<DriverComplianceConfigResponse?> GetAsync()
|
||||
{
|
||||
var config = await repo.GetAsync();
|
||||
return config is null
|
||||
? null
|
||||
: new DriverComplianceConfigResponse(
|
||||
config.Id,
|
||||
config.EnforceLicenseExpiry,
|
||||
config.EnforceMedicalClearance,
|
||||
config.UpdatedAt,
|
||||
config.UpdatedBy);
|
||||
}
|
||||
|
||||
public async Task<DriverComplianceConfigResponse> UpsertAsync(UpdateDriverComplianceConfigRequest request, Guid updatedBy, CancellationToken ct = default)
|
||||
{
|
||||
var config = await repo.GetAsync();
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
if (config is null)
|
||||
{
|
||||
config = new global::Distribution.Models.DriverComplianceConfig
|
||||
{
|
||||
EnforceLicenseExpiry = request.EnforceLicenseExpiry,
|
||||
EnforceMedicalClearance = request.EnforceMedicalClearance,
|
||||
UpdatedBy = updatedBy,
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
await repo.AddAsync(config);
|
||||
}
|
||||
else
|
||||
{
|
||||
config.EnforceLicenseExpiry = request.EnforceLicenseExpiry;
|
||||
config.EnforceMedicalClearance = request.EnforceMedicalClearance;
|
||||
config.UpdatedBy = updatedBy;
|
||||
config.UpdatedAt = DateTime.Now;
|
||||
repo.Update(config);
|
||||
}
|
||||
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
|
||||
return new DriverComplianceConfigResponse(
|
||||
config.Id,
|
||||
config.EnforceLicenseExpiry,
|
||||
config.EnforceMedicalClearance,
|
||||
config.UpdatedAt,
|
||||
config.UpdatedBy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Distribution.Features.DriverComplianceConfig;
|
||||
|
||||
public interface IDriverComplianceConfigRepository
|
||||
{
|
||||
Task<Models.DriverComplianceConfig?> GetAsync();
|
||||
Task AddAsync(Models.DriverComplianceConfig config);
|
||||
void Update(Models.DriverComplianceConfig config);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.DriverProfile;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/driver-profiles")]
|
||||
[Authorize]
|
||||
public class DriverProfileController(DriverProfileService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Authorize(Policy = "driver_profiles.view")]
|
||||
public async Task<IActionResult> GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
var result = await service.GetAllAsync(page, pageSize);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[Authorize(Policy = "driver_profiles.view")]
|
||||
public async Task<IActionResult> GetById(Guid id)
|
||||
{
|
||||
var result = await service.GetByIdAsync(id);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Policy = "driver_profiles.create")]
|
||||
public async Task<IActionResult> Create(CreateDriverProfileRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await service.CreateAsync(request, ct);
|
||||
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Policy = "driver_profiles.update")]
|
||||
public async Task<IActionResult> Update(Guid id, UpdateDriverProfileRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await service.UpdateAsync(id, request, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Policy = "driver_profiles.deactivate")]
|
||||
public async Task<IActionResult> Deactivate(Guid id, CancellationToken ct)
|
||||
{
|
||||
var success = await service.DeactivateAsync(id, ct);
|
||||
return success ? NoContent() : NotFound();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Distribution.Features.DriverProfile;
|
||||
|
||||
public record DriverProfileResponse(
|
||||
Guid Id,
|
||||
Guid UserId,
|
||||
string UserFullName,
|
||||
string LicenseNumber,
|
||||
DateTime LicenseExpiry,
|
||||
DateTime MedicalClearanceExpiry,
|
||||
bool IsActive,
|
||||
DateTime CreatedAt);
|
||||
|
||||
public record CreateDriverProfileRequest(
|
||||
Guid FK_UserId,
|
||||
string LicenseNumber,
|
||||
DateTime LicenseExpiry,
|
||||
DateTime MedicalClearanceExpiry);
|
||||
|
||||
public record UpdateDriverProfileRequest(
|
||||
string LicenseNumber,
|
||||
DateTime LicenseExpiry,
|
||||
DateTime MedicalClearanceExpiry);
|
||||
@@ -0,0 +1,40 @@
|
||||
using Distribution.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.DriverProfile;
|
||||
|
||||
public class DriverProfileRepository(AppDBContext context) : IDriverProfileRepository
|
||||
{
|
||||
public async Task<global::Distribution.Models.DriverProfile?> GetByIdAsync(Guid id) =>
|
||||
await context.DriverProfiles
|
||||
.Include(p => p.User)
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
|
||||
public async Task<global::Distribution.Models.DriverProfile?> GetByUserIdAsync(Guid userId) =>
|
||||
await context.DriverProfiles
|
||||
.Include(p => p.User)
|
||||
.FirstOrDefaultAsync(p => p.FK_UserId == userId);
|
||||
|
||||
public async Task<List<global::Distribution.Models.DriverProfile>> GetAllAsync(int page, int pageSize) =>
|
||||
await context.DriverProfiles
|
||||
.Include(p => p.User)
|
||||
.Where(p => p.IsActive)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<int> GetCountAsync() =>
|
||||
await context.DriverProfiles.CountAsync(p => p.IsActive);
|
||||
|
||||
public async Task AddAsync(global::Distribution.Models.DriverProfile profile) =>
|
||||
await context.DriverProfiles.AddAsync(profile);
|
||||
|
||||
public void Update(global::Distribution.Models.DriverProfile profile) =>
|
||||
context.DriverProfiles.Update(profile);
|
||||
|
||||
public void Deactivate(global::Distribution.Models.DriverProfile profile)
|
||||
{
|
||||
profile.IsActive = false;
|
||||
context.DriverProfiles.Update(profile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Infra.UoW;
|
||||
|
||||
namespace Distribution.Features.DriverProfile;
|
||||
|
||||
public class DriverProfileService(IDriverProfileRepository repo, IUnitOfWork uow)
|
||||
{
|
||||
public async Task<PagedResponse<DriverProfileResponse>> GetAllAsync(int page, int pageSize)
|
||||
{
|
||||
var total = await repo.GetCountAsync();
|
||||
var items = await repo.GetAllAsync(page, pageSize);
|
||||
return new PagedResponse<DriverProfileResponse>(page, pageSize, total,
|
||||
items.Select(ToResponse).ToList());
|
||||
}
|
||||
|
||||
public async Task<DriverProfileResponse?> GetByIdAsync(Guid id)
|
||||
{
|
||||
var profile = await repo.GetByIdAsync(id);
|
||||
return profile is null || !profile.IsActive ? null : ToResponse(profile);
|
||||
}
|
||||
|
||||
public async Task<DriverProfileResponse> CreateAsync(CreateDriverProfileRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var existing = await repo.GetByUserIdAsync(request.FK_UserId);
|
||||
if (existing is not null && existing.IsActive)
|
||||
throw new InvalidOperationException("An active driver profile already exists for this user");
|
||||
|
||||
var profile = new global::Distribution.Models.DriverProfile
|
||||
{
|
||||
FK_UserId = request.FK_UserId,
|
||||
LicenseNumber = request.LicenseNumber,
|
||||
LicenseExpiry = request.LicenseExpiry,
|
||||
MedicalClearanceExpiry = request.MedicalClearanceExpiry
|
||||
};
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await repo.AddAsync(profile);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
|
||||
var saved = await repo.GetByIdAsync(profile.Id);
|
||||
return ToResponse(saved!);
|
||||
}
|
||||
|
||||
public async Task<DriverProfileResponse?> UpdateAsync(Guid id, UpdateDriverProfileRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var profile = await repo.GetByIdAsync(id);
|
||||
if (profile is null || !profile.IsActive) return null;
|
||||
|
||||
profile.LicenseNumber = request.LicenseNumber;
|
||||
profile.LicenseExpiry = request.LicenseExpiry;
|
||||
profile.MedicalClearanceExpiry = request.MedicalClearanceExpiry;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
repo.Update(profile);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
|
||||
return ToResponse(profile);
|
||||
}
|
||||
|
||||
public async Task<bool> DeactivateAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var profile = await repo.GetByIdAsync(id);
|
||||
if (profile is null || !profile.IsActive) return false;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
repo.Deactivate(profile);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static DriverProfileResponse ToResponse(global::Distribution.Models.DriverProfile p) =>
|
||||
new(p.Id, p.FK_UserId, p.User?.FullName ?? string.Empty,
|
||||
p.LicenseNumber, p.LicenseExpiry, p.MedicalClearanceExpiry,
|
||||
p.IsActive, p.CreatedAt);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Distribution.Features.DriverProfile;
|
||||
|
||||
public interface IDriverProfileRepository
|
||||
{
|
||||
Task<global::Distribution.Models.DriverProfile?> GetByIdAsync(Guid id);
|
||||
Task<global::Distribution.Models.DriverProfile?> GetByUserIdAsync(Guid userId);
|
||||
Task<List<global::Distribution.Models.DriverProfile>> GetAllAsync(int page, int pageSize);
|
||||
Task<int> GetCountAsync();
|
||||
Task AddAsync(global::Distribution.Models.DriverProfile profile);
|
||||
void Update(global::Distribution.Models.DriverProfile profile);
|
||||
void Deactivate(global::Distribution.Models.DriverProfile profile);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.Ride;
|
||||
|
||||
public interface IRideCancelRequestRepository
|
||||
{
|
||||
Task<RideCancelRequest?> GetByIdAsync(Guid id);
|
||||
Task<RideCancelRequest?> GetByRideIdAsync(Guid rideId);
|
||||
Task<RideCancelRequest?> GetPendingByRideIdAsync(Guid rideId);
|
||||
Task<List<RideCancelRequest>> GetAllAsync(CancelRequestStatus? status, int page, int pageSize);
|
||||
Task<int> GetCountAsync(CancelRequestStatus? status);
|
||||
Task AddAsync(RideCancelRequest request);
|
||||
void Update(RideCancelRequest request);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.Ride;
|
||||
|
||||
public interface IRideRepository
|
||||
{
|
||||
Task<DeliveryRun?> GetByIdAsync(Guid id);
|
||||
Task<List<DeliveryRun>> GetAllAsync(RideStatus? status, RideType? type, Guid? driverProfileId, Guid? vehicleId, int page, int pageSize);
|
||||
Task<int> GetCountAsync(RideStatus? status, RideType? type, Guid? driverProfileId, Guid? vehicleId);
|
||||
Task<bool> HasOpenOrActiveRideForVehicleAsync(Guid vehicleId, Guid? excludeRideId = null);
|
||||
Task<List<DeliveryRun>> GetActiveRidesAsync(Guid? excludeRideId = null);
|
||||
Task<List<OrderSummaryModel>> GetActiveOrdersByRouteIdAsync(Guid routeId);
|
||||
Task AddAsync(DeliveryRun ride);
|
||||
void Update(DeliveryRun ride);
|
||||
}
|
||||
|
||||
public record OrderSummaryModel(
|
||||
Guid Id,
|
||||
Guid StoreId,
|
||||
string StoreName,
|
||||
DateTime OrderDate,
|
||||
decimal TotalAmount);
|
||||
@@ -0,0 +1,39 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.Ride;
|
||||
|
||||
public class RideCancelRequestRepository(AppDBContext context) : IRideCancelRequestRepository
|
||||
{
|
||||
public async Task<RideCancelRequest?> GetByIdAsync(Guid id) =>
|
||||
await context.RideCancelRequests.FirstOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
public async Task<RideCancelRequest?> GetByRideIdAsync(Guid rideId) =>
|
||||
await context.RideCancelRequests
|
||||
.Where(r => r.FK_RideId == rideId)
|
||||
.OrderByDescending(r => r.RequestedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
public async Task<RideCancelRequest?> GetPendingByRideIdAsync(Guid rideId) =>
|
||||
await context.RideCancelRequests
|
||||
.FirstOrDefaultAsync(r => r.FK_RideId == rideId && r.Status == CancelRequestStatus.Pending);
|
||||
|
||||
public async Task<List<RideCancelRequest>> GetAllAsync(CancelRequestStatus? status, int page, int pageSize) =>
|
||||
await context.RideCancelRequests
|
||||
.Where(r => status == null || r.Status == status)
|
||||
.OrderByDescending(r => r.RequestedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<int> GetCountAsync(CancelRequestStatus? status) =>
|
||||
await context.RideCancelRequests
|
||||
.CountAsync(r => status == null || r.Status == status);
|
||||
|
||||
public async Task AddAsync(RideCancelRequest request) =>
|
||||
await context.RideCancelRequests.AddAsync(request);
|
||||
|
||||
public void Update(RideCancelRequest request) =>
|
||||
context.RideCancelRequests.Update(request);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.Ride;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/rides")]
|
||||
[Authorize]
|
||||
public class RideController(RideService service, ICurrentUserService currentUser) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Authorize(Policy = "rides.view")]
|
||||
public async Task<IActionResult> GetAll(
|
||||
[FromQuery] RideStatus? status,
|
||||
[FromQuery] RideType? type,
|
||||
[FromQuery] Guid? driverProfileId,
|
||||
[FromQuery] Guid? vehicleId,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20)
|
||||
{
|
||||
var result = await service.GetAllAsync(status, type, driverProfileId, vehicleId, page, pageSize);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[Authorize(Policy = "rides.view")]
|
||||
public async Task<IActionResult> GetById(Guid id)
|
||||
{
|
||||
var result = await service.GetByIdAsync(id);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Policy = "rides.create")]
|
||||
public async Task<IActionResult> Create(CreateRideRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await service.CreateAsync(request, ct);
|
||||
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/driver")]
|
||||
[Authorize(Policy = "rides.manage")]
|
||||
public async Task<IActionResult> AssignDriver(Guid id, AssignRideDriverRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.AssignDriverAsync(id, request.FK_DriverProfileId, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/open")]
|
||||
[Authorize(Policy = "rides.manage")]
|
||||
public async Task<IActionResult> Open(Guid id, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.OpenAsync(id, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/activate")]
|
||||
[Authorize(Policy = "rides.manage")]
|
||||
public async Task<IActionResult> Activate(Guid id, ActivateRideRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.ActivateAsync(id, request, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/close")]
|
||||
[Authorize(Policy = "rides.manage")]
|
||||
public async Task<IActionResult> Close(Guid id, CloseRideRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.CloseAsync(id, request, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/cancel-request")]
|
||||
[Authorize(Policy = "rides.cancel.request")]
|
||||
public async Task<IActionResult> RaiseCancelRequest(Guid id, RaiseCancelRequest request, CancellationToken ct)
|
||||
{
|
||||
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||
try
|
||||
{
|
||||
var result = await service.RaiseCancelRequestAsync(id, request, userId, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpGet("cancel-requests")]
|
||||
[Authorize(Policy = "rides.view")]
|
||||
public async Task<IActionResult> GetCancelRequests(
|
||||
[FromQuery] CancelRequestStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20)
|
||||
{
|
||||
var result = await service.GetCancelRequestsAsync(status, page, pageSize);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("cancel-requests/{id:guid}/approve")]
|
||||
[Authorize(Policy = "rides.cancel.approve")]
|
||||
public async Task<IActionResult> ApproveCancelRequest(Guid id, CancellationToken ct)
|
||||
{
|
||||
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||
try
|
||||
{
|
||||
var result = await service.ApproveCancelAsync(id, userId, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("cancel-requests/{id:guid}/reject")]
|
||||
[Authorize(Policy = "rides.cancel.approve")]
|
||||
public async Task<IActionResult> RejectCancelRequest(Guid id, CancellationToken ct)
|
||||
{
|
||||
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||
try
|
||||
{
|
||||
var result = await service.RejectCancelAsync(id, userId, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/cancel-request")]
|
||||
[Authorize(Policy = "rides.view")]
|
||||
public async Task<IActionResult> GetCancelRequest(Guid id)
|
||||
{
|
||||
var result = await service.GetCancelRequestAsync(id);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("breakdown-assist/{rideId:guid}")]
|
||||
[Authorize(Policy = "rides.view")]
|
||||
public async Task<IActionResult> GetBreakdownAssist(Guid rideId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.GetBreakdownAssistAsync(rideId);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Distribution.Models;
|
||||
using Distribution.Features.Stock;
|
||||
|
||||
namespace Distribution.Features.Ride;
|
||||
|
||||
// Ride (DeliveryRun)
|
||||
public record RideResponse(
|
||||
Guid Id,
|
||||
RideType RideType,
|
||||
RideStatus Status,
|
||||
Guid VehicleId,
|
||||
string VehicleNumberPlate,
|
||||
Guid DriverProfileId,
|
||||
string DriverFullName,
|
||||
Guid RouteId,
|
||||
string RouteName,
|
||||
Guid SourceWarehouseId,
|
||||
string SourceWarehouseName,
|
||||
int Meterstart,
|
||||
int Meterend,
|
||||
DateTime? StartedAt,
|
||||
DateTime? ActivatedAt,
|
||||
DateTime? ClosedAt,
|
||||
DateTime CreatedAt);
|
||||
|
||||
public record CreateRideRequest(
|
||||
RideType RideType,
|
||||
Guid FK_VehicleId,
|
||||
Guid FK_DriverProfileId,
|
||||
Guid FK_RouteId,
|
||||
Guid? FK_SourceWarehouseId);
|
||||
|
||||
public record AssignRideDriverRequest(Guid FK_DriverProfileId);
|
||||
|
||||
public record ActivateRideRequest(int Meterstart);
|
||||
|
||||
public record CloseRideRequest(int Meterend);
|
||||
|
||||
public record RaiseCancelRequest(CancelRequestType TypeCode, string Reason);
|
||||
|
||||
// Cancel Request
|
||||
public record RideCancelRequestResponse(
|
||||
Guid Id,
|
||||
Guid RideId,
|
||||
CancelRequestType TypeCode,
|
||||
string Reason,
|
||||
Guid RequestedBy,
|
||||
DateTime RequestedAt,
|
||||
CancelRequestStatus Status,
|
||||
Guid? ApprovedBy,
|
||||
DateTime? ApprovedAt);
|
||||
|
||||
public record BreakdownAssistRideResponse(
|
||||
Guid RideId,
|
||||
Guid VehicleId,
|
||||
string VehicleNumberPlate,
|
||||
Guid DriverProfileId,
|
||||
string DriverFullName,
|
||||
Guid RouteId,
|
||||
string RouteName,
|
||||
List<StockBalanceResponse> StockLevels);
|
||||
|
||||
public record BreakdownAssistOrderResponse(
|
||||
Guid OrderId,
|
||||
Guid StoreId,
|
||||
string StoreName,
|
||||
DateTime OrderDate,
|
||||
decimal TotalAmount);
|
||||
|
||||
public record BreakdownAssistResponse(
|
||||
Guid RideId,
|
||||
List<BreakdownAssistRideResponse> ActiveRides,
|
||||
List<BreakdownAssistOrderResponse> OpenOrders);
|
||||
@@ -0,0 +1,72 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.Ride;
|
||||
|
||||
public class RideRepository(AppDBContext context) : IRideRepository
|
||||
{
|
||||
public async Task<DeliveryRun?> GetByIdAsync(Guid id) =>
|
||||
await context.DeliveryRuns
|
||||
.Include(r => r.Vehicle)
|
||||
.Include(r => r.DriverProfile).ThenInclude(dp => dp.User)
|
||||
.Include(r => r.Route)
|
||||
.Include(r => r.SourceWarehouse)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
public async Task<List<DeliveryRun>> GetAllAsync(RideStatus? status, RideType? type, Guid? driverProfileId, Guid? vehicleId, int page, int pageSize) =>
|
||||
await context.DeliveryRuns
|
||||
.Include(r => r.Vehicle)
|
||||
.Include(r => r.DriverProfile).ThenInclude(dp => dp.User)
|
||||
.Include(r => r.Route)
|
||||
.Include(r => r.SourceWarehouse)
|
||||
.Where(r =>
|
||||
(status == null || r.Status == status) &&
|
||||
(type == null || r.RideType == type) &&
|
||||
(driverProfileId == null || r.FK_DriverProfileId == driverProfileId) &&
|
||||
(vehicleId == null || r.FK_VehicleId == vehicleId))
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<int> GetCountAsync(RideStatus? status, RideType? type, Guid? driverProfileId, Guid? vehicleId) =>
|
||||
await context.DeliveryRuns
|
||||
.CountAsync(r =>
|
||||
(status == null || r.Status == status) &&
|
||||
(type == null || r.RideType == type) &&
|
||||
(driverProfileId == null || r.FK_DriverProfileId == driverProfileId) &&
|
||||
(vehicleId == null || r.FK_VehicleId == vehicleId));
|
||||
|
||||
public async Task<bool> HasOpenOrActiveRideForVehicleAsync(Guid vehicleId, Guid? excludeRideId = null) =>
|
||||
await context.DeliveryRuns.AnyAsync(r =>
|
||||
r.FK_VehicleId == vehicleId &&
|
||||
r.Id != excludeRideId &&
|
||||
(r.Status == RideStatus.Open || r.Status == RideStatus.Active));
|
||||
|
||||
public async Task<List<DeliveryRun>> GetActiveRidesAsync(Guid? excludeRideId = null) =>
|
||||
await context.DeliveryRuns
|
||||
.Include(r => r.Vehicle)
|
||||
.Include(r => r.DriverProfile).ThenInclude(dp => dp.User)
|
||||
.Include(r => r.Route)
|
||||
.Where(r => r.Id != excludeRideId && r.Status == RideStatus.Active)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<List<OrderSummaryModel>> GetActiveOrdersByRouteIdAsync(Guid routeId) =>
|
||||
await context.Orders
|
||||
.Include(o => o.Store)
|
||||
.Where(o => o.IsActive && o.Store.FK_RouteId == routeId)
|
||||
.OrderByDescending(o => o.OrderDate)
|
||||
.Select(o => new OrderSummaryModel(
|
||||
o.Id,
|
||||
o.FK_StoreId,
|
||||
o.Store.StoreName,
|
||||
o.OrderDate,
|
||||
o.TotalAmount))
|
||||
.ToListAsync();
|
||||
|
||||
public async Task AddAsync(DeliveryRun ride) =>
|
||||
await context.DeliveryRuns.AddAsync(ride);
|
||||
|
||||
public void Update(DeliveryRun ride) =>
|
||||
context.DeliveryRuns.Update(ride);
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Features.DriverComplianceConfig;
|
||||
using Distribution.Features.DriverProfile;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.Ride;
|
||||
|
||||
public class RideService(
|
||||
IRideRepository rideRepo,
|
||||
IDriverProfileRepository driverRepo,
|
||||
IDriverComplianceConfigRepository complianceRepo,
|
||||
IRideCancelRequestRepository cancelRepo,
|
||||
Features.Vehicle.IVehicleRepository vehicleRepo,
|
||||
IStockRepository stockRepo,
|
||||
IUnitOfWork uow)
|
||||
{
|
||||
public async Task<PagedResponse<RideResponse>> GetAllAsync(
|
||||
RideStatus? status,
|
||||
RideType? type,
|
||||
Guid? driverProfileId,
|
||||
Guid? vehicleId,
|
||||
int page,
|
||||
int pageSize)
|
||||
{
|
||||
var total = await rideRepo.GetCountAsync(status, type, driverProfileId, vehicleId);
|
||||
var items = await rideRepo.GetAllAsync(status, type, driverProfileId, vehicleId, page, pageSize);
|
||||
return new PagedResponse<RideResponse>(page, pageSize, total, items.Select(ToResponse).ToList());
|
||||
}
|
||||
|
||||
public async Task<RideResponse?> GetByIdAsync(Guid id)
|
||||
{
|
||||
var ride = await rideRepo.GetByIdAsync(id);
|
||||
return ride is null ? null : ToResponse(ride);
|
||||
}
|
||||
|
||||
public async Task<RideResponse> CreateAsync(CreateRideRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var vehicle = await vehicleRepo.GetByIdAsync(request.FK_VehicleId)
|
||||
?? throw new InvalidOperationException("Vehicle not found");
|
||||
|
||||
var sourceWarehouseId = vehicle.FK_DefaultWarehouseId ?? request.FK_SourceWarehouseId;
|
||||
if (!sourceWarehouseId.HasValue)
|
||||
throw new InvalidOperationException("Source warehouse is required for this vehicle");
|
||||
|
||||
var ride = new DeliveryRun
|
||||
{
|
||||
RideType = request.RideType,
|
||||
FK_VehicleId = request.FK_VehicleId,
|
||||
FK_DriverProfileId = request.FK_DriverProfileId,
|
||||
FK_RouteId = request.FK_RouteId,
|
||||
FK_SourceWarehouseId = sourceWarehouseId.Value,
|
||||
Status = RideStatus.Draft
|
||||
};
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await rideRepo.AddAsync(ride);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await uow.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
|
||||
var saved = await rideRepo.GetByIdAsync(ride.Id);
|
||||
return ToResponse(saved!);
|
||||
}
|
||||
|
||||
public async Task<RideResponse> OpenAsync(Guid rideId, CancellationToken ct = default)
|
||||
{
|
||||
var ride = await rideRepo.GetByIdAsync(rideId)
|
||||
?? throw new KeyNotFoundException("Ride not found");
|
||||
|
||||
if (ride.Status != RideStatus.Draft)
|
||||
throw new InvalidOperationException($"Cannot open a ride in {ride.Status} status");
|
||||
|
||||
if (await rideRepo.HasOpenOrActiveRideForVehicleAsync(ride.FK_VehicleId, ride.Id))
|
||||
throw new InvalidOperationException("Vehicle already has an open or active ride");
|
||||
|
||||
await CheckDriverComplianceAsync(ride.FK_DriverProfileId);
|
||||
|
||||
ride.Status = RideStatus.Open;
|
||||
ride.StartedAt = DateTime.Now;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { rideRepo.Update(ride); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse(ride);
|
||||
}
|
||||
|
||||
public async Task<RideResponse> AssignDriverAsync(Guid rideId, Guid driverProfileId, CancellationToken ct = default)
|
||||
{
|
||||
var ride = await rideRepo.GetByIdAsync(rideId)
|
||||
?? throw new KeyNotFoundException("Ride not found");
|
||||
|
||||
if (ride.Status != RideStatus.Draft)
|
||||
throw new InvalidOperationException("Driver can only be changed while the ride is in Draft status");
|
||||
|
||||
var driver = await driverRepo.GetByIdAsync(driverProfileId);
|
||||
if (driver is null || !driver.IsActive)
|
||||
throw new InvalidOperationException("Driver profile not found or inactive");
|
||||
|
||||
ride.FK_DriverProfileId = driverProfileId;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { rideRepo.Update(ride); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
var saved = await rideRepo.GetByIdAsync(rideId);
|
||||
return ToResponse(saved!);
|
||||
}
|
||||
|
||||
public async Task<RideResponse> ActivateAsync(Guid rideId, ActivateRideRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var ride = await rideRepo.GetByIdAsync(rideId)
|
||||
?? throw new KeyNotFoundException("Ride not found");
|
||||
|
||||
if (ride.Status != RideStatus.Open)
|
||||
throw new InvalidOperationException($"Cannot activate a ride in {ride.Status} status");
|
||||
|
||||
ride.Status = RideStatus.Active;
|
||||
ride.ActivatedAt = DateTime.Now;
|
||||
ride.Meterstart = request.Meterstart;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { rideRepo.Update(ride); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse(ride);
|
||||
}
|
||||
|
||||
public async Task<RideResponse> CloseAsync(Guid rideId, CloseRideRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var ride = await rideRepo.GetByIdAsync(rideId)
|
||||
?? throw new KeyNotFoundException("Ride not found");
|
||||
|
||||
if (ride.Status != RideStatus.Active)
|
||||
throw new InvalidOperationException($"Cannot close a ride in {ride.Status} status");
|
||||
|
||||
await EnsureVehicleStockClearedAsync(ride.FK_VehicleId);
|
||||
|
||||
ride.Status = RideStatus.Closed;
|
||||
ride.ClosedAt = DateTime.Now;
|
||||
ride.Meterend = request.Meterend;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { rideRepo.Update(ride); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToResponse(ride);
|
||||
}
|
||||
|
||||
public async Task<RideCancelRequestResponse> RaiseCancelRequestAsync(Guid rideId, RaiseCancelRequest request, Guid requestedBy, CancellationToken ct = default)
|
||||
{
|
||||
var ride = await rideRepo.GetByIdAsync(rideId)
|
||||
?? throw new KeyNotFoundException("Ride not found");
|
||||
|
||||
if (ride.Status != RideStatus.Active)
|
||||
throw new InvalidOperationException($"Cannot raise a cancel request for a ride in {ride.Status} status");
|
||||
|
||||
var existing = await cancelRepo.GetPendingByRideIdAsync(rideId);
|
||||
if (existing is not null)
|
||||
throw new InvalidOperationException("There is already a pending cancel request for this ride");
|
||||
|
||||
var cancelRequest = new RideCancelRequest
|
||||
{
|
||||
FK_RideId = rideId,
|
||||
TypeCode = request.TypeCode,
|
||||
Reason = request.Reason,
|
||||
RequestedBy = requestedBy,
|
||||
RequestedAt = DateTime.Now,
|
||||
Status = CancelRequestStatus.Pending
|
||||
};
|
||||
|
||||
ride.Status = RideStatus.CancelRequested;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
await cancelRepo.AddAsync(cancelRequest);
|
||||
rideRepo.Update(ride);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToCancelResponse(cancelRequest);
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<RideCancelRequestResponse>> GetCancelRequestsAsync(CancelRequestStatus? status, int page, int pageSize)
|
||||
{
|
||||
var total = await cancelRepo.GetCountAsync(status);
|
||||
var items = await cancelRepo.GetAllAsync(status, page, pageSize);
|
||||
return new PagedResponse<RideCancelRequestResponse>(page, pageSize, total, items.Select(ToCancelResponse).ToList());
|
||||
}
|
||||
|
||||
public async Task<RideCancelRequestResponse> ApproveCancelAsync(Guid cancelRequestId, Guid approvedBy, CancellationToken ct = default)
|
||||
{
|
||||
var cancelRequest = await cancelRepo.GetByIdAsync(cancelRequestId)
|
||||
?? throw new KeyNotFoundException("Cancel request not found");
|
||||
|
||||
if (cancelRequest.Status != CancelRequestStatus.Pending)
|
||||
throw new InvalidOperationException("Cancel request is not pending");
|
||||
|
||||
var ride = await rideRepo.GetByIdAsync(cancelRequest.FK_RideId)
|
||||
?? throw new KeyNotFoundException("Ride not found");
|
||||
|
||||
if (ride.Status != RideStatus.CancelRequested)
|
||||
throw new InvalidOperationException("Ride is not in CancelRequested status");
|
||||
|
||||
await EnsureVehicleStockClearedAsync(ride.FK_VehicleId);
|
||||
|
||||
cancelRequest.Status = CancelRequestStatus.Approved;
|
||||
cancelRequest.ApprovedBy = approvedBy;
|
||||
cancelRequest.ApprovedAt = DateTime.Now;
|
||||
|
||||
ride.Status = RideStatus.Cancelled;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
cancelRepo.Update(cancelRequest);
|
||||
rideRepo.Update(ride);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToCancelResponse(cancelRequest);
|
||||
}
|
||||
|
||||
public async Task<RideCancelRequestResponse> RejectCancelAsync(Guid cancelRequestId, Guid rejectedBy, CancellationToken ct = default)
|
||||
{
|
||||
var cancelRequest = await cancelRepo.GetByIdAsync(cancelRequestId)
|
||||
?? throw new KeyNotFoundException("Cancel request not found");
|
||||
|
||||
if (cancelRequest.Status != CancelRequestStatus.Pending)
|
||||
throw new InvalidOperationException("Cancel request is not pending");
|
||||
|
||||
var ride = await rideRepo.GetByIdAsync(cancelRequest.FK_RideId)
|
||||
?? throw new KeyNotFoundException("Ride not found");
|
||||
|
||||
if (ride.Status != RideStatus.CancelRequested)
|
||||
throw new InvalidOperationException("Ride is not in CancelRequested status");
|
||||
|
||||
cancelRequest.Status = CancelRequestStatus.Rejected;
|
||||
cancelRequest.ApprovedBy = rejectedBy;
|
||||
cancelRequest.ApprovedAt = DateTime.Now;
|
||||
|
||||
// Restore to previous status: Active if ActivatedAt is set, else Open
|
||||
ride.Status = ride.ActivatedAt.HasValue ? RideStatus.Active : RideStatus.Open;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
cancelRepo.Update(cancelRequest);
|
||||
rideRepo.Update(ride);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return ToCancelResponse(cancelRequest);
|
||||
}
|
||||
|
||||
public async Task<RideCancelRequestResponse?> GetCancelRequestAsync(Guid rideId)
|
||||
{
|
||||
var cancelRequest = await cancelRepo.GetByRideIdAsync(rideId);
|
||||
return cancelRequest is null ? null : ToCancelResponse(cancelRequest);
|
||||
}
|
||||
|
||||
public async Task<BreakdownAssistResponse> GetBreakdownAssistAsync(Guid rideId)
|
||||
{
|
||||
var strandedRide = await rideRepo.GetByIdAsync(rideId)
|
||||
?? throw new KeyNotFoundException("Ride not found");
|
||||
|
||||
var activeRides = await rideRepo.GetActiveRidesAsync(rideId);
|
||||
var activeRideResponses = new List<BreakdownAssistRideResponse>();
|
||||
|
||||
foreach (var ride in activeRides)
|
||||
{
|
||||
var stockLevels = await stockRepo.GetBalancesByLocationAsync(ride.FK_VehicleId, LocationType.Vehicle);
|
||||
activeRideResponses.Add(new BreakdownAssistRideResponse(
|
||||
ride.Id,
|
||||
ride.FK_VehicleId,
|
||||
ride.Vehicle?.NumberPlate ?? string.Empty,
|
||||
ride.FK_DriverProfileId,
|
||||
ride.DriverProfile?.User?.FullName ?? string.Empty,
|
||||
ride.FK_RouteId,
|
||||
ride.Route?.RouteName ?? string.Empty,
|
||||
stockLevels.Select(b => new StockBalanceResponse(
|
||||
b.FK_ItemId,
|
||||
b.Item.Name,
|
||||
b.LocationId,
|
||||
b.LocationType,
|
||||
b.Quantity,
|
||||
b.UpdatedAt)).ToList()));
|
||||
}
|
||||
|
||||
var openOrders = await rideRepo.GetActiveOrdersByRouteIdAsync(strandedRide.FK_RouteId);
|
||||
|
||||
return new BreakdownAssistResponse(
|
||||
strandedRide.Id,
|
||||
activeRideResponses,
|
||||
openOrders.Select(o => new BreakdownAssistOrderResponse(
|
||||
o.Id,
|
||||
o.StoreId,
|
||||
o.StoreName,
|
||||
o.OrderDate,
|
||||
o.TotalAmount)).ToList());
|
||||
}
|
||||
|
||||
private async Task CheckDriverComplianceAsync(Guid driverProfileId)
|
||||
{
|
||||
var driver = await driverRepo.GetByIdAsync(driverProfileId)
|
||||
?? throw new InvalidOperationException("Driver profile not found");
|
||||
|
||||
if (!driver.IsActive)
|
||||
throw new InvalidOperationException("Driver profile is inactive");
|
||||
|
||||
var config = await complianceRepo.GetAsync();
|
||||
if (config is null) return;
|
||||
|
||||
var today = DateTime.Now.Date;
|
||||
|
||||
if (config.EnforceLicenseExpiry && driver.LicenseExpiry.Date <= today)
|
||||
throw new InvalidOperationException("Driver license has expired");
|
||||
|
||||
if (config.EnforceMedicalClearance && driver.MedicalClearanceExpiry.Date <= today)
|
||||
throw new InvalidOperationException("Driver medical clearance has expired");
|
||||
}
|
||||
|
||||
private async Task EnsureVehicleStockClearedAsync(Guid vehicleId)
|
||||
{
|
||||
var balances = await stockRepo.GetBalancesByLocationAsync(vehicleId, LocationType.Vehicle);
|
||||
if (balances.Any(b => b.Quantity > 0))
|
||||
throw new InvalidOperationException("All vehicle stock must be returned before this ride can be completed");
|
||||
}
|
||||
|
||||
private static RideResponse ToResponse(DeliveryRun r) =>
|
||||
new(r.Id, r.RideType, r.Status,
|
||||
r.FK_VehicleId, r.Vehicle?.NumberPlate ?? string.Empty,
|
||||
r.FK_DriverProfileId, r.DriverProfile?.User?.FullName ?? string.Empty,
|
||||
r.FK_RouteId, r.Route?.RouteName ?? string.Empty,
|
||||
r.FK_SourceWarehouseId, r.SourceWarehouse?.Name ?? string.Empty,
|
||||
r.Meterstart, r.Meterend,
|
||||
r.StartedAt, r.ActivatedAt, r.ClosedAt, r.CreatedAt);
|
||||
|
||||
private static RideCancelRequestResponse ToCancelResponse(RideCancelRequest c) =>
|
||||
new(c.Id, c.FK_RideId, c.TypeCode, c.Reason, c.RequestedBy, c.RequestedAt,
|
||||
c.Status, c.ApprovedBy, c.ApprovedAt);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ public interface IStockRepository
|
||||
{
|
||||
// Balance
|
||||
Task<StockBalance?> GetBalanceAsync(Guid itemId, Guid locationId);
|
||||
Task<List<StockBalance>> GetBalancesByLocationAsync(Guid locationId, LocationType locationType);
|
||||
Task<List<StockBalance>> GetBalancesAsync(Guid? locationId, LocationType? locationType, Guid? itemId, int page, int pageSize);
|
||||
Task<int> GetBalancesCountAsync(Guid? locationId, LocationType? locationType, Guid? itemId);
|
||||
Task<List<StockBalance>> GetBalancesByItemAsync(Guid itemId);
|
||||
|
||||
@@ -11,6 +11,12 @@ public class StockRepository(AppDBContext db) : IStockRepository
|
||||
.Include(b => b.Item)
|
||||
.FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.LocationId == locationId);
|
||||
|
||||
public Task<List<StockBalance>> GetBalancesByLocationAsync(Guid locationId, LocationType locationType) =>
|
||||
db.StockBalances
|
||||
.Include(b => b.Item)
|
||||
.Where(b => b.LocationId == locationId && b.LocationType == locationType)
|
||||
.ToListAsync();
|
||||
|
||||
public Task<List<StockBalance>> GetBalancesAsync(Guid? locationId, LocationType? locationType, Guid? itemId, int page, int pageSize)
|
||||
{
|
||||
var query = db.StockBalances.Include(b => b.Item).AsQueryable();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,257 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Distribution.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RideLifecycle : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "FK_DefaultWarehouseId",
|
||||
table: "Vehicles",
|
||||
type: "char(36)",
|
||||
nullable: true,
|
||||
collation: "ascii_general_ci");
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ActivatedAt",
|
||||
table: "DeliveryRuns",
|
||||
type: "datetime(6)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ClosedAt",
|
||||
table: "DeliveryRuns",
|
||||
type: "datetime(6)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "FK_DriverProfileId",
|
||||
table: "DeliveryRuns",
|
||||
type: "char(36)",
|
||||
nullable: false,
|
||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
|
||||
collation: "ascii_general_ci");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "FK_SourceWarehouseId",
|
||||
table: "DeliveryRuns",
|
||||
type: "char(36)",
|
||||
nullable: false,
|
||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
|
||||
collation: "ascii_general_ci");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "RideType",
|
||||
table: "DeliveryRuns",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "StartedAt",
|
||||
table: "DeliveryRuns",
|
||||
type: "datetime(6)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Status",
|
||||
table: "DeliveryRuns",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DriverComplianceConfigs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
EnforceLicenseExpiry = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
EnforceMedicalClearance = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedBy = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DriverComplianceConfigs", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DriverProfiles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
FK_UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
LicenseNumber = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
LicenseExpiry = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
MedicalClearanceExpiry = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DriverProfiles", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DriverProfiles_Users_FK_UserId",
|
||||
column: x => x.FK_UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RideCancelRequests",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
FK_RideId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
TypeCode = table.Column<int>(type: "int", nullable: false),
|
||||
Reason = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
RequestedBy = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
RequestedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
ApprovedBy = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
ApprovedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RideCancelRequests", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_RideCancelRequests_DeliveryRuns_FK_RideId",
|
||||
column: x => x.FK_RideId,
|
||||
principalTable: "DeliveryRuns",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Vehicles_FK_DefaultWarehouseId",
|
||||
table: "Vehicles",
|
||||
column: "FK_DefaultWarehouseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DeliveryRuns_FK_DriverProfileId",
|
||||
table: "DeliveryRuns",
|
||||
column: "FK_DriverProfileId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DeliveryRuns_FK_SourceWarehouseId",
|
||||
table: "DeliveryRuns",
|
||||
column: "FK_SourceWarehouseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DriverProfiles_FK_UserId",
|
||||
table: "DriverProfiles",
|
||||
column: "FK_UserId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RideCancelRequests_FK_RideId",
|
||||
table: "RideCancelRequests",
|
||||
column: "FK_RideId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DeliveryRuns_DriverProfiles_FK_DriverProfileId",
|
||||
table: "DeliveryRuns",
|
||||
column: "FK_DriverProfileId",
|
||||
principalTable: "DriverProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DeliveryRuns_Warehouses_FK_SourceWarehouseId",
|
||||
table: "DeliveryRuns",
|
||||
column: "FK_SourceWarehouseId",
|
||||
principalTable: "Warehouses",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Vehicles_Warehouses_FK_DefaultWarehouseId",
|
||||
table: "Vehicles",
|
||||
column: "FK_DefaultWarehouseId",
|
||||
principalTable: "Warehouses",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DeliveryRuns_DriverProfiles_FK_DriverProfileId",
|
||||
table: "DeliveryRuns");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DeliveryRuns_Warehouses_FK_SourceWarehouseId",
|
||||
table: "DeliveryRuns");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Vehicles_Warehouses_FK_DefaultWarehouseId",
|
||||
table: "Vehicles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DriverComplianceConfigs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DriverProfiles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RideCancelRequests");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Vehicles_FK_DefaultWarehouseId",
|
||||
table: "Vehicles");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_DeliveryRuns_FK_DriverProfileId",
|
||||
table: "DeliveryRuns");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_DeliveryRuns_FK_SourceWarehouseId",
|
||||
table: "DeliveryRuns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FK_DefaultWarehouseId",
|
||||
table: "Vehicles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ActivatedAt",
|
||||
table: "DeliveryRuns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ClosedAt",
|
||||
table: "DeliveryRuns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FK_DriverProfileId",
|
||||
table: "DeliveryRuns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FK_SourceWarehouseId",
|
||||
table: "DeliveryRuns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RideType",
|
||||
table: "DeliveryRuns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StartedAt",
|
||||
table: "DeliveryRuns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Status",
|
||||
table: "DeliveryRuns");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2910
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace Distribution.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ridelife : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Distribution.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RidePermissions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,12 +79,24 @@ namespace Distribution.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("ActivatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime?>("ClosedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid>("FK_DriverProfileId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("FK_RouteId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("FK_SourceWarehouseId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("FK_VehicleId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
@@ -97,15 +109,84 @@ namespace Distribution.Migrations
|
||||
b.Property<int>("Meterstart")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RideType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FK_DriverProfileId");
|
||||
|
||||
b.HasIndex("FK_RouteId");
|
||||
|
||||
b.HasIndex("FK_SourceWarehouseId");
|
||||
|
||||
b.HasIndex("FK_VehicleId");
|
||||
|
||||
b.ToTable("DeliveryRuns");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.DriverComplianceConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<bool>("EnforceLicenseExpiry")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("EnforceMedicalClearance")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("DriverComplianceConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.DriverProfile", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid>("FK_UserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime>("LicenseExpiry")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("LicenseNumber")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("MedicalClearanceExpiry")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FK_UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DriverProfiles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.Item", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -568,6 +649,86 @@ namespace Distribution.Migrations
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Description = "Delete categories and subcategories",
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("20000000-0000-0000-0000-000000000039"),
|
||||
Code = "driver_profiles.view",
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Description = "View driver profiles",
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("20000000-0000-0000-0000-000000000040"),
|
||||
Code = "driver_profiles.create",
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Description = "Create driver profiles",
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("20000000-0000-0000-0000-000000000041"),
|
||||
Code = "driver_profiles.update",
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Description = "Update driver profiles",
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("20000000-0000-0000-0000-000000000042"),
|
||||
Code = "driver_profiles.deactivate",
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Description = "Deactivate driver profiles",
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("20000000-0000-0000-0000-000000000043"),
|
||||
Code = "compliance.manage",
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Description = "Manage driver compliance configuration",
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("20000000-0000-0000-0000-000000000044"),
|
||||
Code = "rides.view",
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Description = "View rides",
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("20000000-0000-0000-0000-000000000045"),
|
||||
Code = "rides.create",
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Description = "Create rides",
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("20000000-0000-0000-0000-000000000046"),
|
||||
Code = "rides.manage",
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Description = "Manage ride lifecycle",
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("20000000-0000-0000-0000-000000000047"),
|
||||
Code = "rides.cancel.request",
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Description = "Request ride cancellation",
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("20000000-0000-0000-0000-000000000048"),
|
||||
Code = "rides.cancel.approve",
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Description = "Approve or reject ride cancellations",
|
||||
IsActive = true
|
||||
});
|
||||
});
|
||||
|
||||
@@ -658,6 +819,44 @@ namespace Distribution.Migrations
|
||||
b.ToTable("Regions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.RideCancelRequest", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("ApprovedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid?>("ApprovedBy")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("FK_RideId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("RequestedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid>("RequestedBy")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("TypeCode")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FK_RideId");
|
||||
|
||||
b.ToTable("RideCancelRequests");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.Role", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -1029,6 +1228,86 @@ namespace Distribution.Migrations
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000039"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("0a391e23-6b25-1d96-1dec-432a8ac4908a"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000040"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("36a6baec-5d5a-4451-78cd-42851c0cf1c3"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000041"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("e43de642-1786-7189-559a-0932277ebfb9"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000042"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("0d4fb9be-3948-8620-97a0-c69466f4607b"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000043"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("ea913dba-03e5-5f02-50cb-9edf60bb91ed"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000044"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("0d18aa7a-e2cd-bb4f-897a-ad6858e91a8f"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000045"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("58305a8f-f40e-d668-ca41-f8807cf97c6d"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000046"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("1b35af7e-8268-23b5-11f4-0e549200ade9"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000047"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("838d6f2c-f0d0-8b36-3a91-16cadccdae6e"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000048"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("541d54d5-ce03-7a5f-8810-f1d650747120"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000023"),
|
||||
@@ -1157,6 +1436,86 @@ namespace Distribution.Migrations
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000039"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("a66f3854-5a8f-95c2-cb1c-b2b2e67ee226"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000040"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("14bcecd1-b1a2-6da3-155a-233611484467"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000041"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("5468aac5-b826-a754-7d73-10b3f552162d"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000042"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("ddb0c721-79c6-ea5a-1f45-a78e8ec6afa8"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000043"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("8543b8fe-3006-292d-d20c-d122cc84a1ce"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000044"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("1485ac7a-924d-e856-d2af-edc7e3ba6801"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000045"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("119f5e59-46ab-6a7c-fd0c-e74a161f6ad6"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000046"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("74c6f06e-0ca0-acb4-c027-d4fff5ef325f"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000047"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("aad2bfde-98d1-9ed5-fe2f-f9533f0983e3"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000048"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("b4fc0664-1199-9df8-e1c8-b3b4377dd90e"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000004"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000006"),
|
||||
@@ -1391,7 +1750,87 @@ namespace Distribution.Migrations
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000001"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000039"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("aeb29e0e-e819-1446-6cc6-1fb64b5ecc6b"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000040"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("e18a6eca-1009-bcc6-35e9-ead943c1649f"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000041"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("c33c55f0-eaee-80ad-3aa2-562ee9f5f6dc"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000042"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("ce64f1be-6287-5c43-3aac-9b6c24c1cc1e"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000043"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("7a5da517-7722-2a3b-351a-90ecd4b7684d"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000044"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("b54cfa0f-c4f8-573d-012d-d63611594ca9"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000045"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("66857379-93f6-7c6c-506d-b92df56a3bd8"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000046"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("6f96d257-a401-2297-a443-e282dd3a26fe"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000047"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("482942f4-6582-91d3-2f66-a3679829d922"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000048"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("c02ac74f-ec06-c162-b852-261d230ade14"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000031"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("3e3e6f06-72c8-42fa-f7e6-8feeaca36d60"),
|
||||
IsActive = true
|
||||
@@ -1787,6 +2226,70 @@ namespace Distribution.Migrations
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("dc3dd9ff-b123-a8d4-dcea-b4f5f8ff171e"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000041"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("49f7fcdb-12a9-4353-f80d-972fa407a754"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000042"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("0339cb4b-76f5-4ecd-4a4a-4a97f3504677"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000043"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("9cc907d6-a01c-2c38-1542-4fb6a0ea362d"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000044"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("64c47d3b-020b-fa33-19df-f8b47dce8c26"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000045"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("a5ba999b-d935-1f16-8dba-7acc32a3964f"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000046"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("7549f095-2ab7-d332-1f36-0210cf4ef0a8"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000047"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("5cd89e65-145e-e4a1-921c-d7d4621d4723"),
|
||||
IsActive = true
|
||||
},
|
||||
new
|
||||
{
|
||||
FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"),
|
||||
FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000048"),
|
||||
CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
Id = new Guid("f9de2655-9424-2588-1a51-96cb1a41fc6b"),
|
||||
IsActive = true
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2242,6 +2745,9 @@ namespace Distribution.Migrations
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid?>("FK_DefaultWarehouseId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
@@ -2258,6 +2764,8 @@ namespace Distribution.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FK_DefaultWarehouseId");
|
||||
|
||||
b.ToTable("Vehicles");
|
||||
});
|
||||
|
||||
@@ -2296,23 +2804,50 @@ namespace Distribution.Migrations
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.DeliveryRun", b =>
|
||||
{
|
||||
b.HasOne("Distribution.Models.DriverProfile", "DriverProfile")
|
||||
.WithMany()
|
||||
.HasForeignKey("FK_DriverProfileId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Distribution.Models.Route", "Route")
|
||||
.WithMany()
|
||||
.HasForeignKey("FK_RouteId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Distribution.Models.Warehouse", "SourceWarehouse")
|
||||
.WithMany()
|
||||
.HasForeignKey("FK_SourceWarehouseId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Distribution.Models.Vehicle", "Vehicle")
|
||||
.WithMany()
|
||||
.HasForeignKey("FK_VehicleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("DriverProfile");
|
||||
|
||||
b.Navigation("Route");
|
||||
|
||||
b.Navigation("SourceWarehouse");
|
||||
|
||||
b.Navigation("Vehicle");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.DriverProfile", b =>
|
||||
{
|
||||
b.HasOne("Distribution.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("FK_UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.Item", b =>
|
||||
{
|
||||
b.HasOne("Distribution.Models.Uom", "BaseUom")
|
||||
@@ -2374,32 +2909,15 @@ namespace Distribution.Migrations
|
||||
b.Navigation("Order");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.PriceList", b =>
|
||||
modelBuilder.Entity("Distribution.Models.RideCancelRequest", b =>
|
||||
{
|
||||
b.HasOne("Distribution.Models.Region", "Region")
|
||||
b.HasOne("Distribution.Models.DeliveryRun", "Ride")
|
||||
.WithMany()
|
||||
.HasForeignKey("FK_RegionId");
|
||||
|
||||
b.Navigation("Region");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.PriceListItem", b =>
|
||||
{
|
||||
b.HasOne("Distribution.Models.Item", "Item")
|
||||
.WithMany()
|
||||
.HasForeignKey("FK_ItemId")
|
||||
.HasForeignKey("FK_RideId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Distribution.Models.PriceList", "PriceList")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("FK_PriceListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Item");
|
||||
|
||||
b.Navigation("PriceList");
|
||||
b.Navigation("Ride");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.RolePermission", b =>
|
||||
@@ -2589,6 +3107,16 @@ namespace Distribution.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.Vehicle", b =>
|
||||
{
|
||||
b.HasOne("Distribution.Models.Warehouse", "DefaultWarehouse")
|
||||
.WithMany()
|
||||
.HasForeignKey("FK_DefaultWarehouseId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("DefaultWarehouse");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Distribution.Models.Warehouse", b =>
|
||||
{
|
||||
b.HasOne("Distribution.Models.Vehicle", "Vehicle")
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Distribution.Models
|
||||
{
|
||||
public class DeliveryRun : Template
|
||||
{
|
||||
public RideType RideType { get; set; }
|
||||
public RideStatus Status { get; set; } = RideStatus.Draft;
|
||||
|
||||
public Guid FK_VehicleId { get; set; }
|
||||
[ForeignKey("FK_VehicleId")]
|
||||
public Vehicle Vehicle { get; set; }
|
||||
//public Guid FK_DriverId { get; set; }
|
||||
//[ForeignKey("FK_DriverId")]
|
||||
//public Driver Driver { get; set; }
|
||||
public int Meterstart { get; set; }
|
||||
public int Meterend { get; set; }
|
||||
|
||||
public Guid FK_DriverProfileId { get; set; }
|
||||
[ForeignKey("FK_DriverProfileId")]
|
||||
public DriverProfile DriverProfile { get; set; }
|
||||
|
||||
public Guid FK_RouteId { get; set; }
|
||||
[ForeignKey("FK_RouteId")]
|
||||
public Route Route { get; set; }
|
||||
|
||||
public Guid FK_SourceWarehouseId { get; set; }
|
||||
[ForeignKey("FK_SourceWarehouseId")]
|
||||
public Warehouse SourceWarehouse { get; set; }
|
||||
|
||||
public int Meterstart { get; set; }
|
||||
public int Meterend { get; set; }
|
||||
|
||||
public DateTime? StartedAt { get; set; }
|
||||
public DateTime? ActivatedAt { get; set; }
|
||||
public DateTime? ClosedAt { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Distribution.Models
|
||||
{
|
||||
public class DriverComplianceConfig
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public bool EnforceLicenseExpiry { get; set; } = false;
|
||||
public bool EnforceMedicalClearance { get; set; } = false;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.Now;
|
||||
public Guid UpdatedBy { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Distribution.Models
|
||||
{
|
||||
public class DriverProfile : Template
|
||||
{
|
||||
public Guid FK_UserId { get; set; }
|
||||
[ForeignKey("FK_UserId")]
|
||||
public User User { get; set; }
|
||||
|
||||
public string LicenseNumber { get; set; }
|
||||
public DateTime LicenseExpiry { get; set; }
|
||||
public DateTime MedicalClearanceExpiry { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -20,4 +20,12 @@ namespace Distribution.Models
|
||||
Damage,
|
||||
Adjustment
|
||||
}
|
||||
|
||||
public enum RideType { Delivery, Sale }
|
||||
|
||||
public enum RideStatus { Draft, Open, Active, CancelRequested, Cancelled, Closed }
|
||||
|
||||
public enum CancelRequestType { VehicleBreakdown, DriverUnavailable, RouteIssue, Other }
|
||||
|
||||
public enum CancelRequestStatus { Pending, Approved, Rejected }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Distribution.Models
|
||||
{
|
||||
public class RideCancelRequest
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
public Guid FK_RideId { get; set; }
|
||||
[ForeignKey("FK_RideId")]
|
||||
public DeliveryRun Ride { get; set; }
|
||||
|
||||
public CancelRequestType TypeCode { get; set; }
|
||||
public string Reason { get; set; }
|
||||
|
||||
public Guid RequestedBy { get; set; }
|
||||
public DateTime RequestedAt { get; set; } = DateTime.Now;
|
||||
|
||||
public CancelRequestStatus Status { get; set; } = CancelRequestStatus.Pending;
|
||||
public Guid? ApprovedBy { get; set; }
|
||||
public DateTime? ApprovedAt { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
namespace Distribution.Models
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Distribution.Models
|
||||
{
|
||||
public class Vehicle : Template
|
||||
{
|
||||
public string RegNumber { get; set; }
|
||||
public string NumberPlate { get; set; }
|
||||
public VehicleType Type { get; set; }
|
||||
|
||||
public Guid? FK_DefaultWarehouseId { get; set; }
|
||||
[ForeignKey("FK_DefaultWarehouseId")]
|
||||
public Warehouse DefaultWarehouse { get; set; }
|
||||
}
|
||||
public enum VehicleType
|
||||
{
|
||||
|
||||
@@ -3,10 +3,13 @@ using Distribution.Data;
|
||||
using Distribution.Features.Auth;
|
||||
using Distribution.Features.Brand;
|
||||
using Distribution.Features.Category;
|
||||
using Distribution.Features.DriverComplianceConfig;
|
||||
using Distribution.Features.DriverProfile;
|
||||
using Distribution.Features.Item;
|
||||
using Distribution.Features.Order;
|
||||
using Distribution.Features.PriceList;
|
||||
using Distribution.Features.Region;
|
||||
using Distribution.Features.Ride;
|
||||
using Distribution.Features.Role;
|
||||
using Distribution.Features.Route;
|
||||
using Distribution.Features.Store;
|
||||
@@ -49,6 +52,10 @@ builder.Services.AddScoped<IBrandRepository, BrandRepository>();
|
||||
builder.Services.AddScoped<IUomRepository, UomRepository>();
|
||||
builder.Services.AddScoped<IItemRepository, ItemRepository>();
|
||||
builder.Services.AddScoped<IStockRepository, StockRepository>();
|
||||
builder.Services.AddScoped<IDriverProfileRepository, DriverProfileRepository>();
|
||||
builder.Services.AddScoped<IDriverComplianceConfigRepository, DriverComplianceConfigRepository>();
|
||||
builder.Services.AddScoped<IRideRepository, RideRepository>();
|
||||
builder.Services.AddScoped<IRideCancelRequestRepository, RideCancelRequestRepository>();
|
||||
builder.Services.AddScoped<IStoreKycDocumentRepository, StoreKycDocumentRepository>();
|
||||
builder.Services.AddScoped<IPriceListRepository, PriceListRepository>();
|
||||
builder.Services.AddScoped<ISupplierRepository, SupplierRepository>();
|
||||
@@ -71,6 +78,9 @@ builder.Services.AddScoped<StockService>();
|
||||
builder.Services.AddScoped<StoreKycDocumentService>();
|
||||
builder.Services.AddScoped<PriceListService>();
|
||||
builder.Services.AddScoped<SupplierService>();
|
||||
builder.Services.AddScoped<DriverProfileService>();
|
||||
builder.Services.AddScoped<DriverComplianceConfigService>();
|
||||
builder.Services.AddScoped<RideService>();
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
|
||||
|
||||
@@ -1,82 +1,251 @@
|
||||
# 03 — Ride Lifecycle
|
||||
|
||||
**Status**: Not Started
|
||||
**Status**: Done
|
||||
**Depends On**: 02 — Stock Ledger
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Turn the currently-unused `DeliveryRun` model into a working ride: a session that turns a vehicle
|
||||
into a temporary warehouse. Covers `assignVehicleToRoute`, `assignDriverToVehicle`,
|
||||
`checkDriverCompliance` from [mainPlan.md §5](./mainPlan.md#5-vehicle--fleet-management)
|
||||
and `planDeliveryRoute` from §3.
|
||||
|
||||
---
|
||||
|
||||
## Background / Context
|
||||
|
||||
`Models/DeliveryRun.cs` exists (`FK_VehicleId`, `FK_RouteId`, `Meterstart`, `Meterend`) but has no
|
||||
repository, service, or controller — it is completely unwired today, per [mainPlan.md](./mainPlan.md)'s
|
||||
"Known Gaps" table. `FK_DriverId`/`Driver` navigation is commented out because no `Driver` entity
|
||||
exists yet (gap #10 in [00_existing_gaps.md](./00_existing_gaps.md)) — this step is blocked on that
|
||||
being resolved first if driver assignment is required at ride-open time.
|
||||
Turn the currently-unused `DeliveryRun` model into a fully working ride — a session that turns a
|
||||
vehicle into a temporary warehouse. Covers driver profile management, ride creation, state
|
||||
transitions, cancellation workflow, and the vehicle breakdown scenario.
|
||||
|
||||
---
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
### Ride (extending DeliveryRun)
|
||||
### New Entity — DriverProfile
|
||||
|
||||
Add a `Status` field to `DeliveryRun` for the state machine below, and a `RideType` field
|
||||
(`Delivery` | `Sale`).
|
||||
Linked one-to-one with `User`. Holds compliance data only — no login or auth fields.
|
||||
|
||||
### Ride State Machine
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| Id | Guid | PK |
|
||||
| FK_UserId | Guid | FK → User (unique) |
|
||||
| LicenseNumber | string | |
|
||||
| LicenseExpiry | DateTime | |
|
||||
| MedicalClearanceExpiry | DateTime | |
|
||||
| IsActive | bool | Can be suspended without deleting |
|
||||
| CreatedAt | DateTime | |
|
||||
|
||||
`Draft → Open → Active → Closed`. A vehicle can only be on one `Open`/`Active` ride at a time —
|
||||
enforced in the service layer, not the database.
|
||||
No license class tied to vehicle type — not required.
|
||||
|
||||
### Ride Type (Delivery vs Sale)
|
||||
**Compliance check rule:** Driver is compliant if `IsActive = true`, plus any checks that are enabled in `DriverComplianceConfig`. By default all checks are disabled. When enabled: `LicenseExpiry > today` AND/OR `MedicalClearanceExpiry > today`.
|
||||
|
||||
`Delivery` rides fulfill pre-confirmed orders (see [05_delivery_ride.md](./05_delivery_ride.md));
|
||||
`Sale` rides create invoices on the spot with no pre-orders (see [06_sale_ride.md](./06_sale_ride.md)).
|
||||
Both share the same ride lifecycle and loading event.
|
||||
---
|
||||
|
||||
### Updated Entity — Vehicle
|
||||
|
||||
Add one nullable field:
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| FK_DefaultWarehouseId | Guid? | If set, ride always loads from this warehouse. If null, warehouse is selected at ride creation. |
|
||||
|
||||
---
|
||||
|
||||
### Updated Entity — DeliveryRun (becomes Ride)
|
||||
|
||||
Add the following fields to the existing `DeliveryRun`:
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| RideType | RideType | Delivery or Sale |
|
||||
| Status | RideStatus | State machine below |
|
||||
| FK_DriverProfileId | Guid | FK → DriverProfile |
|
||||
| FK_SourceWarehouseId | Guid | Where stock is loaded from. Must be the same warehouse stock returns to on cancel/close. |
|
||||
| StartedAt | DateTime? | Set when Draft → Open |
|
||||
| ActivatedAt | DateTime? | Set when Open → Active |
|
||||
| ClosedAt | DateTime? | Set when Active → Closed |
|
||||
|
||||
---
|
||||
|
||||
### New Enum — RideType
|
||||
|
||||
```csharp
|
||||
public enum RideType { Delivery, Sale }
|
||||
```
|
||||
|
||||
### New Enum — RideStatus
|
||||
|
||||
```csharp
|
||||
public enum RideStatus { Draft, Open, Active, CancelRequested, Cancelled, Closed }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### New Entity — RideCancelRequest
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| Id | Guid | PK |
|
||||
| FK_RideId | Guid | FK → DeliveryRun |
|
||||
| TypeCode | CancelRequestType | Reason category |
|
||||
| Reason | string | Free text explanation |
|
||||
| RequestedBy | Guid | UserId |
|
||||
| RequestedAt | DateTime | |
|
||||
| Status | CancelRequestStatus | Pending, Approved, Rejected |
|
||||
| ApprovedBy | Guid? | UserId of approver |
|
||||
| ApprovedAt | DateTime? | |
|
||||
|
||||
### New Enum — CancelRequestType
|
||||
|
||||
```csharp
|
||||
public enum CancelRequestType
|
||||
{
|
||||
VehicleBreakdown,
|
||||
DriverUnavailable,
|
||||
RouteIssue,
|
||||
Other
|
||||
}
|
||||
```
|
||||
|
||||
### New Entity — DriverComplianceConfig
|
||||
|
||||
Single-row configuration table. Controls which compliance checks are enforced at ride open time.
|
||||
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| Id | Guid | PK |
|
||||
| EnforceLicenseExpiry | bool | Default false. If true, ride open is blocked if driver license is expired. |
|
||||
| EnforceMedicalClearance | bool | Default false. If true, ride open is blocked if medical clearance is expired. |
|
||||
| UpdatedAt | DateTime | |
|
||||
| UpdatedBy | Guid | UserId of last person who changed the config. |
|
||||
|
||||
Managed via a dedicated config endpoint. Only one row ever exists.
|
||||
|
||||
---
|
||||
|
||||
### New Enum — CancelRequestStatus
|
||||
|
||||
```csharp
|
||||
public enum CancelRequestStatus { Pending, Approved, Rejected }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Machine
|
||||
|
||||
```
|
||||
Draft → Open → Active → Closed
|
||||
↘ CancelRequested → Cancelled
|
||||
```
|
||||
|
||||
| Transition | Trigger | What it does |
|
||||
|---|---|---|
|
||||
| Draft → Open | `POST /api/rides/{id}/open` | Validates driver compliance. Checks vehicle has no other Open/Active ride. Sets `StartedAt`. |
|
||||
| Open → Active | `POST /api/rides/{id}/activate` | Called by loading event confirmation (Step 04). Sets `ActivatedAt`. |
|
||||
| Active → Closed | `POST /api/rides/{id}/close` | Called after ride reconciliation (Step 07). All stock must be returned to source warehouse. Sets `ClosedAt`. |
|
||||
| Active → CancelRequested | `POST /api/rides/{id}/cancel-request` | Driver submits cancel request with type code and reason. Ride status changes to `CancelRequested`. |
|
||||
| CancelRequested → Cancelled | `POST /api/rides/cancel-requests/{id}/approve` | Supervisor approves. Ride status changes to `Cancelled`. All stock must return to source warehouse before cancel completes. |
|
||||
| CancelRequested → Active | `POST /api/rides/cancel-requests/{id}/reject` | Supervisor rejects. Ride returns to `Active`. |
|
||||
|
||||
**Order assignment window:** Orders can be assigned to a Delivery ride from Draft until the ride transitions to Open. No new orders after Open.
|
||||
|
||||
---
|
||||
|
||||
## Warehouse Configuration
|
||||
|
||||
Per vehicle. A vehicle either has `FK_DefaultWarehouseId` set (always loads from that warehouse)
|
||||
or it is null (warehouse is selected manually at ride creation time).
|
||||
|
||||
Loading and unloading warehouse are always the same — stock must return to its source warehouse
|
||||
before a ride can close or cancel.
|
||||
|
||||
---
|
||||
|
||||
## Vehicle Breakdown Scenario
|
||||
|
||||
1. Driver submits cancel request with `TypeCode = VehicleBreakdown`.
|
||||
2. Ride status moves to `CancelRequested`.
|
||||
3. Supervisor views a breakdown assist endpoint that lists all currently `Active` rides with their
|
||||
current vehicle stock levels and the stranded ride's open orders — supervisor decides manually
|
||||
which ride takes over which orders.
|
||||
4. Supervisor approves the cancel request.
|
||||
5. Ride moves to `Cancelled` once all stock is confirmed returned to source warehouse.
|
||||
|
||||
The system surfaces the information — it does not automatically reassign orders.
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Function reference | Endpoint intent |
|
||||
|---|---|
|
||||
| `assignVehicleToRoute(vehicleId, routeId)` (§5) | Implicit in `POST /api/rides` — a ride binds a vehicle to a route for its duration |
|
||||
| `assignDriverToVehicle(vehicleId, driverId)` (§5) | `PUT /api/rides/{id}/driver` — blocked on Driver entity, see gap #10 |
|
||||
| `checkDriverCompliance(driverId)` (§5) | Called internally before a ride can move `Draft → Open` |
|
||||
| `planDeliveryRoute(warehouseId, orderIds[])` (§3) | `POST /api/rides` (for `RideType = Delivery`) |
|
||||
| — | `POST /api/rides/{id}/open`, `POST /api/rides/{id}/close` — state transitions |
|
||||
Base route: `/api/rides`
|
||||
|
||||
| Method | Route | Description |
|
||||
|---|---|---|
|
||||
| POST | `/` | Create ride (Draft). If vehicle has default warehouse, pre-populate it. |
|
||||
| GET | `/` | List rides (paginated). Filter by status, type, driverProfileId, vehicleId. |
|
||||
| GET | `/{id}` | Get ride detail |
|
||||
| PUT | `/{id}/driver` | Assign or change driver profile |
|
||||
| POST | `/{id}/open` | Transition Draft → Open |
|
||||
| POST | `/{id}/activate` | Transition Open → Active (called by loading event) |
|
||||
| POST | `/{id}/close` | Transition Active → Closed (called by reconciliation) |
|
||||
| POST | `/{id}/cancel-request` | Submit cancel request (Active rides only) |
|
||||
| GET | `/cancel-requests` | List cancel requests (paginated). Filter by status. |
|
||||
| POST | `/cancel-requests/{id}/approve` | Approve cancel request |
|
||||
| POST | `/cancel-requests/{id}/reject` | Reject cancel request |
|
||||
| GET | `/breakdown-assist/{rideId}` | Lists active rides with stock + stranded ride's open orders |
|
||||
|
||||
### DriverComplianceConfig
|
||||
|
||||
Base route: `/api/driver-compliance-config`
|
||||
|
||||
| Method | Route | Description |
|
||||
|---|---|---|
|
||||
| GET | `/` | Get current config |
|
||||
| PUT | `/` | Update config flags |
|
||||
|
||||
---
|
||||
|
||||
### DriverProfile
|
||||
|
||||
Base route: `/api/driver-profiles`
|
||||
|
||||
| Method | Route | Description |
|
||||
|---|---|---|
|
||||
| POST | `/` | Create driver profile (links to a User) |
|
||||
| GET | `/` | List driver profiles (paginated) |
|
||||
| GET | `/{id}` | Get driver profile detail |
|
||||
| PUT | `/{id}` | Update compliance fields |
|
||||
| DELETE | `/{id}` | Soft deactivate |
|
||||
|
||||
---
|
||||
|
||||
## Business Rules
|
||||
|
||||
- A ride cannot open without a compliant driver (once Driver entity exists) and a vehicle not
|
||||
already on an open ride.
|
||||
- Opening a ride creates the vehicle's stock ledger location (see [02_stock_ledger.md](./02_stock_ledger.md)).
|
||||
|
||||
---
|
||||
|
||||
## Fraud / Control Considerations
|
||||
|
||||
- `checkDriverCompliance` failures must block ride open, not just warn.
|
||||
1. A ride cannot transition Draft → Open if `IsActive = false` on the driver profile. If `EnforceLicenseExpiry = true`, also blocks if license is expired. If `EnforceMedicalClearance = true`, also blocks if medical clearance is expired.
|
||||
2. A vehicle cannot be on more than one Open or Active ride at a time.
|
||||
3. Orders can only be assigned to a Delivery ride while it is in Draft status.
|
||||
4. Loading and unloading warehouse must be the same — enforced at close/cancel.
|
||||
5. A CancelRequested ride cannot close or activate until the request is resolved.
|
||||
6. All cancel requests must have a TypeCode — free text reason alone is not sufficient.
|
||||
7. All list endpoints return paginated responses.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Is a `Driver` a new entity distinct from `User`, or is "driver" just a `User` with a specific
|
||||
`Role`? Affects whether gap #10 needs a new table or just a role addition.
|
||||
1. **Cancel request type codes** — ✅ Resolved. `VehicleBreakdown`, `DriverUnavailable`, `RouteIssue`, `Other`.
|
||||
2. **Breakdown assist endpoint** — ✅ Resolved. System shows info only, supervisor decides manually. No auto-reassignment.
|
||||
3. **Driver compliance** — ✅ Resolved. Checks are configurable via `DriverComplianceConfig`. `IsActive` is always enforced. License and medical checks are optional flags, default off.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
> Fill this section in during planning discussion before writing any code.
|
||||
**Build order:**
|
||||
1. ✅ Enums — `RideType`, `RideStatus`, `CancelRequestType`, `CancelRequestStatus` → added to `Models/Enums.cs`
|
||||
2. ✅ Entities — `DriverProfile`, `DriverComplianceConfig`, `RideCancelRequest` (new); updated `Vehicle` (+`FK_DefaultWarehouseId`), updated `DeliveryRun` (+`RideType`, `Status`, `FK_DriverProfileId`, `FK_SourceWarehouseId`, `StartedAt`, `ActivatedAt`, `ClosedAt`)
|
||||
3. ✅ `AppDBContext` — new DbSets + Fluent config
|
||||
4. ✅ Migrations — `RideLifecycle` (schema), `RidePermissions` (10 new permissions seeded across all roles)
|
||||
5. ✅ DTOs — `DriverProfileDTOs.cs`, `DriverComplianceConfigDTOs.cs`, `RideDTOs.cs`
|
||||
6. ✅ Repositories — `IDriverProfileRepository`, `IDriverComplianceConfigRepository`, `IRideRepository`, `IRideCancelRequestRepository`
|
||||
7. ✅ Services — `DriverProfileService`, `DriverComplianceConfigService`, `RideService`
|
||||
8. ✅ Controllers — `DriverProfileController`, `DriverComplianceConfigController`, `RideController`
|
||||
9. ✅ DI registration in `Program.cs`
|
||||
|
||||
**Feature folders:** `Features/Ride/`, `Features/DriverProfile/`, `Features/DriverComplianceConfig/`
|
||||
|
||||
**Pending:** `dotnet ef database update` to apply migrations to DB. Tests for the three new services.
|
||||
|
||||
@@ -38,7 +38,7 @@ They must be resolved before new feature steps begin because later steps depend
|
||||
| 00 | Existing Gap Fixes | Done | — | [00_existing_gaps.md](./00_existing_gaps.md) |
|
||||
| 01 | Item Catalog | Done | 00 | [01_item_catalog.md](./01_item_catalog.md) |
|
||||
| 02 | Stock Ledger | Done | 01 | [02_stock_ledger.md](./02_stock_ledger.md) |
|
||||
| 03 | Ride Lifecycle | Not Started | 02 | [03_ride_lifecycle.md](./03_ride_lifecycle.md) |
|
||||
| 03 | Ride Lifecycle | Done | 02 | [03_ride_lifecycle.md](./03_ride_lifecycle.md) |
|
||||
| 04 | Loading Event | Not Started | 03 | [04_loading_event.md](./04_loading_event.md) |
|
||||
| 05 | Delivery Ride Execution | Not Started | 04 | [05_delivery_ride.md](./05_delivery_ride.md) |
|
||||
| 06 | Sale Ride Execution | Not Started | 04 | [06_sale_ride.md](./06_sale_ride.md) |
|
||||
|
||||
Reference in New Issue
Block a user