name: CI/CD - ERP Core Monorepo

on:
  pull_request:
    branches:
      - production
    types:
      - closed

jobs:
  backend-build-and-deploy:
    name: Build & Deploy ERP Core Backend
    runs-on: linux
    if: github.event.pull_request.merged == true
    steps:
      - name: Checkout code
        run: |
          git clone \
            --depth 1 \
            --branch production \
            https://oauth2:${{ secrets.ERPCORE }}@gitea.hexdive.com/New_REP_SYSTEM/ERP-core.git \
            .

      - name: Restore backend
        run: dotnet restore Backend/ERPCore.sln

      - name: Publish backend
        run: |
          rm -rf /tmp/erpcore-deploy
          mkdir -p /tmp/erpcore-deploy
          dotnet publish Backend/ERPCore/ERPCore.csproj \
            -c Release \
            -r linux-x64 \
            --self-contained true \
            -o /tmp/erpcore-deploy

      - name: Stop backend service
        run: systemctl stop erpcore.service

      - name: Copy backend to deploy folder
        run: |
          mkdir -p /home/erp/core/publish
          rm -rf /home/erp/core/publish/*
          cp -r /tmp/erpcore-deploy/* /home/erp/core/publish/

      - name: Fix backend ownership and permissions
        run: |
          chown -R erpuser:erpuser /home/erp/core/publish
          find /home/erp/core/publish -type d -exec chmod 755 {} +
          find /home/erp/core/publish -type f -exec chmod 644 {} +
          chmod +x /home/erp/core/publish/ERPCore

      - name: Restart backend service
        run: systemctl restart erpcore.service

      - name: Verify backend
        run: |
          systemctl is-active erpcore.service && echo "Backend service running" || echo "Backend service failed"

  frontend-build-and-deploy:
    name: Build & Deploy ERP Core Frontend
    runs-on: linux
    needs: backend-build-and-deploy
    if: github.event.pull_request.merged == true
    steps:
      - name: Checkout code
        run: |
          git clone \
            --depth 1 \
            --branch Production \
            https://oauth2:${{ secrets.ERPCORE }}@gitea.hexdive.com/New_REP_SYSTEM/ERP-core.git \
            .

      - name: Build frontend Docker image
        run: docker build -t erpcore-frontend:latest Frontend/erp-system

      - name: Stop & remove existing frontend container
        run: |
          docker stop erpcore-frontend 2>/dev/null || true
          docker rm erpcore-frontend 2>/dev/null || true

      - name: Start frontend container
        run: |
          docker run -d \
            --name erpcore-frontend \
            -p 6702:6702 \
            --add-host=host.docker.internal:host-gateway \
            -e BACKEND_ORIGIN=http://host.docker.internal:6701 \
            --restart unless-stopped \
            --memory="512m" \
            --cpus="1.0" \
            --cap-drop ALL \
            --security-opt no-new-privileges \
            erpcore-frontend:latest

      - name: Remove old frontend images
        run: docker image prune -f

      - name: Verify frontend
        run: |
          docker ps --filter name=erpcore-frontend --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" \
            | grep erpcore-frontend && echo "Frontend container running" || echo "Frontend container failed"
This commit is contained in:
Kalana B. Thilakarathna
2026-09-17 13:59:05 +05:30
parent 93f2f1bd3b
commit 03d01d4c4e
61 changed files with 2195 additions and 1245 deletions
@@ -0,0 +1 @@
False
@@ -0,0 +1 @@
bf7d0202-9a86-43b3-8323-402073a8c61a
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTyW7bMBD9FYFnUaFIaqFuSeygRpDaiI226I3LKCaikAZFZUHgfy8oO0jTWy8EMeCbecvwHTn5BKhDl4fD8hlcHFGOop1LlNAaE4HLZkeqjrcdF0VZcUoZ+41yZG/hDXWo4VqXWgBmUAPmhlRYsV5jY0CUpDSEVjz1lA8j6t6RtIWBZ6uh8OMPCKP1DnXop3XGv6TZ0hZ68JMpgh9g5cYonU5kFsvt7W69wTdN/X15dXV6OcKYGhTWoA61VUW10AJzISrMdcOx7BuKe6r6UpVNRQScYNZFCE4OxWgePzkYHx1E3dGCsoJgKtAxR0ZGmWgrOcLu7ZCYzDYtUj2fy/O1e0fPEFBH8w9DT+0u9GAvnkY12cFcnM4ox8ek9BD8AUK0MPsySx0GGa132XnS9WDBRZSj9TbbDDL2Pjx9MWt70r/6D/m7NH35CnqKYK795GLCohzdSb23DrLVAnWoLrnhghjMSyUxb3qBJTCJVVUL3ipmakihXnsXrZv8NGYrF+EhzPRRh27kMKZp6212GfTeRtBxCknTrzoBN8GbScfs0/2SFKRghCXQFA9TzO7B2AA6QlK3C9O53z8QWlNCEpUpBHAx28i4z77JcZ8emMZUmgKYkqmGqB6q1rRVqxtT123da15pwspKc65Zr4yQSgmmKae94tDUKEeQws7m/Wr6viW1ajEXjcJcMo6VIBzzsq4kV9JUMiE+fczWQ8KpvjGEEoqFbGvMmWK4ZZRhTihpmGx1XaZV+rDknD1PmdxCcDD8JfnO6uBH38fsvATZFxPuJ5f+bjYvxIt1+HV2e+H1I4QspSWtS2t6Duh4PB7/AAAA//8DAAY6R+UCBAAA
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTyU7dMBT9lcjrOHiKHWfHqCJEQYDaqjvHvuFZBPvJcRiE3r9XDg9RuuvOuvI5PsP1GwrmEVCPDrfb0ycIeUY1yn4dMcIkJhpTdUfaXnS90A2VHVdK/UY18hfwinqkhLXUasAcJGDhSIsHPlrsHGhKqCOsFYXT3M+of0PGNw6evIUmzj8gzT4G1KOfPrj4XN42vrFTXFyT4gTnYc4m2CLm5PT24u7qGp8p+f306Oj95gxzIWi8Qz3q2pZZbTUWWrdYWCWwGRXDIxtGOlDVEg3vMB8ypGCmZnYPnxpczAGy7VnDeEMw02hXI2eyKbIHM8Pd67YoWWM6KfN6Ha/H/g09QUI9qz8Cfac7sJM/sPHx0QR3MPrg5w2q0TbFLaTsYY1kdTlNJvsYqv0jx5OHkFGNrm6r68nkMabHLzndvls//w/nl8ZufIDq/AT1SFLhhCYOCzoYLNSosQFu8NBKLbqBOwmltuMYsg9LXObqPGS4T6tK1KMzM82F9Oq2Okx24zPYvKQi/ZcsQHjx+Ti6MiCoRtcpusXm6jNuShrScMILx5K3S65uwPkENkPxdJeWPf0/ECYZKYzHS0oQcnVt8qb6ZuZNueCUay0DcJQPigwjtJ3r2s4qJ2UnRytaSzhtrRCWj4PTZhg0t0ywcRCgZNFd2q3WhQJCmNCK4lGqFgupKDbcScwJjAoYpa4riM9Yq6up4IZROcIIw9p0Egs+cNxxxrEgjChuOitp2Z2PSPaNiw7V6AJSgOkvy5fepjjHMVf76qsvIdwsoXzWal2DZx/wyxr+SbQPkKpSnvGh7OW+r91ut/sDAAD//wMAfxRohPMDAAA=
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTS0/jMBD+K5HPdXBsJ3FyA1q0FWKpoNpd7c0eT6hFcCrH4SHU/75yWsSyt73E0cgz8738Trx+QtKS8/1+9Yw+jmRBoptLnPGKsoYW9ZaVrVStbHKlmkpU8jdZEHeNb6QltQQooEEqsEIqLSupER1Qa7EpWGEZL2WaqR9G0r4T7XKLzw4wH8YfGEY3eNKSn87b4SXt1i6HfphsHoYe136M2kMCs1zdX29vN/Sqrr6vLi6ON0cc04DcWdKSqkOjrC5p3TRAJTSGqrqpqdaisx2W6Ty2OR8xeN3no338xGCH6DFCy3MuckZ5Qw4LYnXUCbbRI27f9gnJLNMy1Rdzef5t38kzBtLyxYegx3Fn0Luzp9FMrrdnx2/U42Niug/DHkN0OOsyU+17Hd3gs9Omy96hj2RBbu+zTa9jN4SnL2LdH/mv/4P+Nm1fvSJMEe3lMPlIWqLIgtxo2DmP2XqZhhXSyoZZKgujqay7hmoUmpqyaqQywlaYTL0cfHR+GqYxW/uID2GGT1pypfsRj8DPA+xcRIhTSJx+ValxEwY7Qcw+1S9YznLBRGqa4n6K2R1aFxAiJnbbMJ3m/dPCK85YgjKFgD5mGx132Tc97tIFW9sSOKIthKmZ6bBUVpUKaltVqupAlsBEUYKUIDpjG21MI4BL3hmJdUUWBJPZ2ZwvbQ2zrKip6rim0uqCKgBGrapBAGCBUHzRMbvtU5/pass447TRqqJSGEGV4IJKxlkttIKqSFH6kOTkvUyeXGPw2P9F+cZBGMahi9kpBNkXEe4mn95uNgfixXn6Oqu9HOARQ5bc0s6nmJ4MOhwOhz8AAAD//wMACwdnnQIEAAA=
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTS2/cIBSF/4rF2jgYsA3e5alGUZooGbVVdzyuExQHRoDzUDT/vcIzVZvuusHoinM5fOf6A3n1DGhEx9vt+Qv4nFCNsltLlNAeE4nbYUO6kYuRy0aInhJJfqIauSt4RyMauDGtkYAZ9IC5JR3WbDLYWpAtaS2hHS891UNC4wdSrrHw4gw0IX2DmFzwaETfnbfhtdytXGPmsNgmhhkufcrKm2Lm7Pz+anNziy+G/uv5ycn+ZIJUGjTOohH1E2hhVYcHKQ3mRmosBjlgpdhkJ+jKdy9zPkP0am6SffrjwYbsIZuRNpQ1BFOJdjWyKqtiW6sEm/dtcbJiOiv1ei2v2/EDvUBEI61/A923OzKzO3pOenGzPdqvWaWntK6TMjnEd1SjbQxbiNnByug4JXjW8/tGpaeL/ZmyTedvYJYM9jQsPqMRCVSjFdE8q+yCrw4OT2cHPqMa3dxXt7PKU4jPnyDf77ld/ge2a2UenYfq8qxoWm65JBbzVivMh0liBUxh3fWSC81sDyXz0+Cz80tYUnXpMzzE1SUa0YWaE+z9HUfz6DKYvMRi/UdfhLcx2MXk6k84LWlIwwgroiVvl1zdgXURTIbyiE1cDv3+kdCeElKsLDGCz9Wtyo/VF5UeywE72M5QANsyPRA9QSes6IQZbN+LfjK8M4S1neHcsElbqbSWzFBOJ81h6FGNoMxCtY6fsUQTbQQeqLGYc9liwcSAmSJaU2HEpOUnjtXNXHR6GiyhhGKpRI850wwLRhnmhJKBKWH6tkzabySHiHnJ/gqih/mvJ187E0MKU64OWVefINwtvvza1Zr7q/P4baV9FswTxKqkpZwvU3wIaLfb7X4BAAD//wMA4dUdUiEEAAA=
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyhTJMUP3ZLYQV0j9SJeNEVvQ3JkC9FSC4raNDD2vxfUbuCmt5xIDPgOZx6+nFcS4YCkJ++Ox7sTxryQmuRxC3HGFWWWtnrPul6aXtpGWGN5K/8mNRkf8DvpiZbet94iFaiQysA66sTgaQhoW9YGxjtZcsLzQvpXAmMT8DR6bOblT0zLOEfSky9jDPO3cjeMjZ/mNTRpnvA+LhmiL8Xc3j097B939KNWf9y9f385ueBSEjRjID1RAzoToKPaWk+lt44abTUFEEMYsCvrRTbGjCnC1Czh61sNYc4Rs+95w0XDKLfkXJMAGUrZDhbcfz+WSjZMtyVeb+Ft27+SEybS8/oH0Eu6Gz+NN3k+TnjC6QhpwXTj58MBYiA1Oab5iCmPuKHZup0myOMcq+tlH6YRYyY1eXyqdhPkYU6Hn3g9XRDc/wKBEyZHetKZDkSrEWQwaMOgOuS2E6ZlKCUIzQODYZBK+KCYVQMCD156r1vOgxWK1OR5mh1Mvy8bP8uZM4FprjhKtFwqZT1DYXDoNLIASjllvA3CaC5NMAIsQGsdam+VLyb5BP5ljFjd35ZuWhmkZYHK1gGVerAUUAB1nbLSOBEUFs2HOeYxrvO6VPcx43Pa+JGefIRpwQu5d8m/jBl9XlOB+pcqwl2aw+pz9eaAljWsEUwU0ZqPa64+YxgT+owF7z6t13z/k3DFGSulrClhzNUO8kv1Gywv5UDQofMcMbTCaeYG7EwwnfE6KGXU4GXnmWg7L6UXgwsWnLPCc8kHJ1EXylgMV20ed8xLaNFT7V1HJVhDQYKhxjDDW9VxZouv3jhWj9OmG3RgnHFqwSgqhRPUCC6oZJxpAcarttj5B5Kr+aQhNXnAFHH6T8ufRp/mZR5ydXVh9ROEz2ss86PaHPltjPSfjfbt7L9iqsprwRjLV7k+0LkmB4RlTXjYxk//Snbln1T7bQpx1TBjTE2eMqS8Hq/hrmsMk+fz+fwvAAAA//8DALVuXjbCBAAA
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRU207kOBD9lcivGwffEtt5A7rRIsRF063d1b75UgEvaaflODAI9b+vkoYBZrQP+xJHJVfVqXOO6xVFswPUotP9fv0EMY+oRDksIUZYg4nGVG5J3QrVCl0pzZlk9d+oROEKXlCLpHCOOg2YQwNYeFJjyzuHvQdNCfWE1WKuae5H1L4iEyoPT8FBNYx/QBrDEFGL/gzRD89zbxMq1w+Tr9LQw2Ucs4luBrNab662t3f4QjY367Oz480RxrlAFTxqUdOBVd7UWGrtsHDaYiW1xMbwzndQz+cxLcQMKZq+Gv3jBwY/5AjZtaxivCKYaXQokTfZzLCtGWH7sp+RLDSt5ni5hJff9hU9QUItK98JPZY7cX042Y12Cr0/Wb6oRPs07CHlAAsjy5B9b3IYYvHW47wPEDMq0dmcspmcg3FELdqmCd6j63gfIlwks4PnIT3eHNtWN+stKtFdGv4Bl+9MfkAt4p1TvrOOGqFtZ8HrhtQGtOmEFarznlPXOUY70khpmei48taappG1t5YxVKLbTXHXm9wNafdFr81Rgsv/ocDm9Hwdje1hzrkw/fhjpK1J95BRiyylnVNSa+FZrT0H4xQI2lhPjFIcBHgute6o4pxxRTtFrNKmVsTWIDxHJbocN9lEb/ohwvo7uGnm94PCa+MeQoTicjUDp8ILTTwW1BosZKexAW6wrRstlOW+gdnD50PMIU7DNBaXMcN9WjT7NMTtpjhN7iFkcHlKsxx/NXPiXRr85HLxYTZKKlJxMgO9nfJ+ysU38CGBywsrbyBvNz+nsIYRMkOZUoKYi1ng4nczzipTL33tGICn3EpiO6iVV7Vy0jeNajonakc4rZ0QjnfWa2Ot5o4J1lkBskElgtnbxfKcrGkceEewB6OwUKrBhmuPQVsKXBkpmPjCY3HbL3md9IQRhrVRDRbccqw441gQRiQ3yjXUfLXwpxFV1VRUVFyqhZp33t5ehVCoRFeQIvSfeLkOLg3j0OXizZXFF6Y+NVqFcd+bl1/6/QaSk456K9/vnz+Ae/zVpN+mOO/GYnH7c4j4+yLvanCPkIrZHibEeQ38yLie+hy2DwmMB389ePi56KFEOzDjlGC3bN9lIURIC4zVdLTYZbwOfR9GcEP0I2oFUxUlgr6h/c9rmlVSE3U4HA7/AgAA//8DADlZ+rXpBQAA
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxTTW/bOBD9K8KcRYUSqQ/q1sQJNkhbB7V3u9gbP0YOEYU0KCptEPi/F5Sdtm572JOE0ejNm/fevIKTTwg9vNvvr5/RxQlyiHYpVbRqCBWkbLe07nnXc1Hwpu2qlv4HOdg7fIEeWq51qQUShg0SbmhNFBs0MQZFSUtDq5onTLmboH8FaQuDz1Zj4ad/MEzWO+jhs3XGf0mzpS306GdTBD/irZuidDqRWV1v7rbre3LTNh+vLy+PnRNOCaCwBnpoBlSdkTVphdCEa6FI14qWSMkGM2CdnsffrIsYnByLyTz+4GB8dBh1XxUVKyipBBxyMDLKRFvJCbcv+8RkkWmV6vlSXl77V3jGAH2Vvwl6hLvQo714mtRsR3Mx+t3Out2Vd4PdzUHGNDiHffB7DNHiItB6k/3gVNKCFlVTUQo5fJD6wTrMblfZekwbq6E1tKIVEbJrCGeKkY5VjHBa0ZbJTjdlInmHweH4E+oHq4Of/BCzk+7Z2Zwr76J1s5+n7NZF3J2I9nAjxwlTwxwCupjdy/iQ/SWnh8TUtKbWFaIpmWqpGrDuTFd3ujVN0zWD5rWmrKw155oNygiplGC64tWgOLYN5LDF8GSdHN/73S5p+X3e+YcVDnIe48bPYUnGZB4hh0vrZHg5dvw9oTl1fTya8Ya0BGocl4Wyk59Xo0UXz/RNaSq54YIawkslCW8HQSQySVTdCN4pZhpMsb4P3sw6/moZowxy+DS7dErZbTLri3Xka8N/ofoTN0y5ypYoCymVarQgjFEkvCkrIriQhCGTkg+a8UFADiuvHzFkyS9p3RnYepO9C/rBRtRxDmnNf5fZ6012P8o4+PB0dnbrOe7nmH1CYwPqiInFNswJ6saO+BvZt71PGvIOctgcr3HZ9n8e45V3k/8D/B8dhx7kHD0cDofDNwAAAP//AwDB5+uJuAQAAA==
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy07cMBT9lcjrODi248TZwQyoCFEQjNqqOz9uGItgjxyHh9D8e+XMIEp3XcW68jk+j5t35NUToB6d7nbnz+DThEqU3DKihApMJK7bDWl63vVcVpIwSrr2NyqRu4I31KOWG1MbCZiBAMwtabBmg8HWgqxJbQlteOZUDxPq35FylYVnZ6AK0w+Ikwse9ein8za85LeVq8wYZlvFMMKln5LyJotZn99fbW5u8UUrvp+fnR1uTjBlgspZ1CMxgO6sanArpcHcSI27VrZYKTbYAZr8PcCcTxC9GqvJPn5qsCF5SKanFWUVwVSifYmsSirL1mqCzdsuK1liWud5uYyXY/+OniGinpYfgR7oTszoTkx4elLengzOu2mLSrSLYQcxOVgiWVyOo0ou+OL4yGp04BMq0c19cTuqNIT49CWn+4P1y/9wfq3M1nkoLtcZU3PLJbGY11ph3g4SK2AK60ZI3mlmBeTaVsEn5+cwT8WlT/AQF5WoRxdqnOCg7zSarUtg0hyz9F8iA+HVpVWweUBQiW5jsLNJxWfcNalIxQjLHHPazam4A+simATZ0ybOR/p/IFRQkhlXc4zgU3Gr0rb4pqZtvmBb2xgKYGumW6IHaDrbNZ1prRCdGAxvDGF1Yzg3bNBWKq0lM5TTQXNoRdad2y2WhSINlxKswMKQBnMuNdaSMtxo0tq2YVTorP4z1uJmzDg9tJZQQrFUncCcaYY7RhnmhJKWqc6IOu/ORyTHxnmHSnQF0cP4l+VrZ2KYwpCKY/XFlxDuZp9/1mJZgxfn8esS/jqYR4hFLk85n/fy2Nd+v9//AQAA//8DAB8jFwPzAwAA
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUS2/dNhCF/4rAtSjzJT60S2IHdY3URmw0RXdDcmQL0aUuKMppYNz/XlC+gZvusiMGPMPDj4fzQhIckAzk3fF49YyprKQlZdpLgglNmaPcPLB+UHboWccsF9rwv0lLphv8TgZiVAg8OKQSNVIVWU+9HAONER1nPDLRq9oTHlcyvBCYuojPU8BuWf/EvE5LIgP5MqW4fKtnw9SFedlil5cZr9NaIIVq5vLq/ubh9o5+NPqPq/fvX3euuNYG3RTJQKKzIVghqTGipwowUDcCUAAZIcKoneOvsikVzAnmbo1f3zzEpSQsYRCdkB2jwpFTSyIUqLY9rPjw/Vid7Jgua73dy/tyeCHPmMkg2h9AX9tdhHm6KMtxxmecj5BXzBdhORwgRdKSY16OmMuEO5r9tvMMZVpScz7swzxhKqQlt/fN3QxlXPLhJ173rwiuf4HAM2ZPBiJZL7UxqKSU4LQXDKPRQWvhAzAzeoE8IOudMo4bxqUTzMleG2siMBdISx7nxcP8+7rzc4J5G5kRWqBCJ5TWLjCUFsfeIIugtdc2uCitEcpGK8EBcOfRBKdDDcknCE9Twub6kgxEcxWVY5Eq7oEqMzoKKIH6XjtlvYwaq+bDksqUtmVbm+tU8DHv/MhAPsK84iu5dzk8TQVD2XKF+peuwru8xC2U5i0BnHWsk0xW0VaOW2k+Y5wyhoIV70Pezv3+JxFaMFatbDljKs0dlKfmN1if6oZoYh8EYuTSG+ZH7G20vQ0mam31GFQfmOR9UCrI0UcH3jsZhBKjV2g0aQnWwDV7xsMYjRo9Uo9CUqUsp1b3hkouNToEiLuRN47N7Vx1fjSRCSaoA6upkl5SK2sHJpiRYIPmNc4/kJzDpyxpyQ3mhPN/rvxpCnlZl7E05xQ2P0H4vKU6P5o9kd+mRP/ZaV8u4Svmpr4WTKl+lfMDnVpyQFi3jId9/Awv5K7+k+Zhn0JCdZpz05L7Arlsx3NZ6c4xrU6n0+lfAAAA//8DAMJGy6DDBAAA
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUy27dNhCGX0XgWpR5v2iXxA5qGKmN+CAtuuNlFBPRoQ4oymlgnHcvKJ/ATXfdEQPO8J9vfs4Lyu4IaETvTqebZ8h1RT2qaQ8xwhQmFlN9IHIUZpRkoMRYQsVfqEfpDn6gEWkRAg0WMAcFWEQisedTwDGCpYRGwqRoNd3XFY0vyKUhwnMKMCzrFyhrWjIa0R8px+V7e9ulIczLFoeyzHCb1+pyaGKubx7vDvcP+KNWv9+8f/96c4W1FRhSRCOK1oRgGMdaM4mFg4Dt5Bx2jkcX3aSspa9pKVco2c3DGr+9aYhLzVDDyAbGB4KZReceRVddk+3dCocfp6Zkx3Td4v0e3o/jC3qGgkbW/wT6Wu4qzOlq3fwMzzCfXFmhXIXleHQ5oh6dynKCUhPsaJ6h+GVNtVENQTtjKI3GaSF1NFwyriMnEA3RMtponRJqYkHKKXpjtZhEZFREBUJ4jXq0w5tnV9OSu4v2D3OCXFGP7h+7h9nVaSnHX/A/vhK9/R9Am2w0Ik4kV1qD4Jw7qzwjELUKSjEfHNGTZ0ADEGmFtlQTyi0jlkuljY6O2IB69MmFp5Shu71GI1JURGFJxIJ6h4WeLHbAHfZSWWE8b42iHn1Yck15W7a1u80Vvpa9XzSij25e4bXTdyU8pQqhbqVB+FO1xIeyxC3U7s0AlAxk4IS3pK2ettp9hpgKhAoNx6Fsl3r/SWGKEdKkbKVArt2Dq0/db259aheijjIwgEi518RPIE000gQdlTJqCkIGwqkMQgQ++Wid95YHJtjkBWiFegTNb91u8TBJyi112BhtsIheYi8Nw0QqYo0xOoo2kTeO3f3c8vykI2GEYeuMwoJ7jg1nHAvCiObOBEWbm38iuZhFGNSjOygZ5n+1/CmFsqzLVLuLa7pfIHzeclsf3e6g7ynjv3fa10v4BqVr03Ipt59yGdC5R0dw61bguG+f8QU9tG/SHfYlxMSgKNU9eqyu1O10CQs1WKLE+Xw+/wMAAP//AwC8ws6OwgQAAA==
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoyUTW/bOBCG/4owZ1GhSOrz1sQJNsi2Tmu3u9gbRY4cIgppkFS6QeD/vqDsbOp2D3sTRqN3nnlnRq9g5RNCDx/2++tntDFADtEsIUZZTWhHymZLq160fUWLsmmrqhJ/QQ7mDl+gh0YoVaoOCccaidC0IgMfFdEau5KWmrJKJE25C9C/gjSFxmejsHDhG/pgnIUe/jBWu++ptjSFmtysC+8mvLUhSqsSzOp6c7dd35Obpv50fXl5zAwYkkBhNPSgu1aplnHSNKwiQqIi3SglkZJrqeVYd115/MzYiN7KqQj68Z1Bu2gxqp4VjBeUsA4OOWgZZcIeZMDtyz6RLDatUjxfwstj/wrP6KFn+ZuhR7kLNZmLpzDMZtIXk9vtjN1dOTua3exlTIVz2Hu3Rx8NLgatN9k7U0kLWrCaUQo5fJTqwVjMblfZekodD2OjKaOMdLKtieADJy1nnAjKaMNlq+oyQd6htzj9oPrRKO+CG2N28j07q3PlbDR2dnPIbm3E3Qm0hxs5BUwJs/doY3Yv40P2mwwPiVQ3ulIMUZd8aOgwYtXqtmpVo+u6rUclKkV5WSkhFB8H3clh6Lhigo2DwKaGHLbon4yV0+9ut0te/lvv/MUKRzlPceNmv2xG0I+Qw6Wx0r8cM74G1KesT8dhvCktCzVNS0PZaZ5Xk0Ebz/yFHupSaNFRTUQ5SCKasSMSuSRDVXeiHbiuMa31vXd6VvHnkXHKIYcvs02nlN2mYX03lvxdi59Qf2DDtFfZssp1Q5EPoyZIO0GEGJFIHCnRVOMoq6qsaAc5rJx6RJ+leUljz8TWm+yDVw8mooqzT23+udReb7L7ScbR+aezs1vPcT/H7Atq41FFTBRbPyepGzPhL7BvfZ88FO1xb4J7y/2GfnDBxPSL+DwbTAZvjue62PE/r/VM853pPzcCepBzdHA4HA7/AAAA//8DAL7u70zYBAAA
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUy27jOBD8FYFnUaFI6mVgDxknwQbZjI3Ys7vYGx/NmIhMGhSVTBDk3weU5YkzmMPeCLIfVdXVfENO7AEt0OXhcP0MLg4oR9FOV5TQGpMOl82WVAveLipS1C1rSMn/Qzmyd/CKFqjhSpWqA8ygBsw1qbBkRmGtoStJqQmteKopHge0eEPCFhqerYLCD39DGKx3aIH+sU77l9Rb2EL1ftRF8D3cuiEKpxKYq+vN3Xa1xjdN/fX6y5dj5ABDKlBYjRZId61SLWW4aWiFuQCFOyMEFoJpoYWpu648plkXITjRF4N++sCgfXQQ1YIWlBUE0w6950iLKBJsKQbYvh4Skkmmq3SfT9fTcfGGniGgBc1Pgh7LXajeXuwHOdpeX0QRHiGaIPbw4sMTPIse5egQ/AFCtDDps9pkH5BKUpCC1pQQlKN7oXbWQXZ7la36RFiaRhNKKO5EW2POJMMtowxzQknDRKvqMmG8g+CgP6t6b1Xwgzcxm2XPPvVZehetG/04ZLcuwmMQ8Zh3I/oBUI7Wo+ztsHsAoV+3/mFMb27sE5flGAK4mK1F3GV/imGXSOhGV4oC6JLJhkgDVavbqlWNruu2NopXirCyUpwrZqTuhJQdU5RTIzk0NcrRaoyHMZ70/34GYhvsfg/6A8F20njdi2h82N9qcNEam0ZziphM1fcTq2yuuewtuPhJZLRAdck174jGvJQC88Z0WAATWFZ1x1vJdA3J2kvvjH0cf+pEjNGcmapta9nJkgA1TctJYyTRyjDV6UpCQxvStNB2ihBZ8bKVnSIayorrxC54Par4qxUYYShHD6NLG5rdJtov1uHvNf9QZB189Mr3aWC2h83rEGH/u6kt/f7gBxsTfzOP9tsA996l1/kPmDWbky99PAuGtAjZtHtJFg2c4k7VDHNtGiwp0biuW97UXDDQHcrRlVdPELLkMGHdNJSTqVab7DKonY2g4hhS638nVsd53px25kOQ4uv1dukDXB4O+Xz7x3NXJAevNtnJAJ/+lm9Dit75IbGIYTzz0QZ6c4J15qaj8bIH0DaAitPT9pg4a/Q7h12GaI1QcUhr8JdXkzFmp80hv/Y7G8DP7GPzsyHMppgr8XaqM/2AkxX+1wf4/v7+/gMAAP//AwCxdSWE9QUAAA==
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTU/cMBD9K9Gc4+DYjvNxoyxVEWpBsGqr3ib2pFgEZ+U4fAjtf6+cXUTprRfLGvnNvPfm+RU8PhB0cLrbnT+SjzPkEN1aElxoxltW1ltedarpKl00Uopa6l+Qg7ukF+igVsaUpiUmSRNTllesl4Nh1lJb8tJyUanUE3/P0L0CusLSozNUTPN3CrObPHTww3k7PaXZ6AozTostwjTShZ8jepPIbM5vL7dX1+xzrb+df/p0eDnTnBoUzkIHtm2MaYRkdS0qppAMawdEhigtWhx025YHmPORgsexmO39Owc7RU/RdKIQsuBMtLDPwWLERLvHmbYvu8RktWmT6vlaXq/dKzxSgE7kb4Ye2p2Y0Z08zP3iRntyOCPO90npLkw7CtHR6ssqdRwxuslnx0lnoyMfIYer2+x6xDhM4eGDWbcH/Rf/IX+bpp8/k1ki2bNp8RE6UAJy+IrmznnKLjbQgS6VVS23TJU9MlUPLUOSyPpKt6rppdWUtno2+ej8Mi1zduEj/Q4rf+jgM44zHZifBnPnIpm4hCTqp07A6zDZxcTs3f6SF7yQXCbQEndLzG7IukAmUpK3Dcux3z8QoQXnicoSAvmYXWO8y77gfJce2NpWRhDZUvY17weqGttUjamt1o0ejKoMl2VllDJy6G2Lfd9KI5QYekW1hhwobTtbA9YS15wImUbNmdKVYNhzYkZz5FXbN7ZMmXj3MbsaE64fassFF6zFRjMle8kaKSRTXPBaYmP0inuz5Lh81UAOlxQ8jX9J/upMmOZpiNkxBdkHE24Wnz5vtibiyXn2vLq9mcw9hSxtC51POT0uaL/f7/8AAAD//wMAd4cViwMEAAA=
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTXW/bIBT9KxbPwcWAMfitbVotqrpGbbRNe8Nw3aA6EGHcD1X57xNOqq5725t1xTn3fFy/I693gFp0vt9fPYNPI1qg5OYRJVRgonDVbEjdctnWopSMM0bVb7RA7gbeUIsabkxlFGAGAjC3pMYd6w22FlRFKktozTOnfhxR+460Ky08OwNlGH9AHF3wqEU/nbfhJe/WrjRDmGwZwwArPybtTRazvHq42dyt8XUjvl9dXBxfjjBmgtJZ1CKrpDGSMtw0tMZcg8Gq1xprzay2uhdKVUeY8wmi10M52qdPDTYkD8m0tKSsJJgqdFggq5POsjs9wuZtn5XMMS3zfDGP58/2HT1DRC1dfAR6pDszgzszYbfT3p71zrtxixZoH8MeYnIwRzK7HAadXPDFacnl4MAntEB3D8V60KkPcfclp4ej9dV/OL/VZus8FKslapGouOWKWMyrTmPe9AprYBp3tVBcdswKyLVdBp+cn8I0Fiuf4DHOKlGLrvUwwlHfeTRbl8CkKWbpv0QGwqtLl8HmQd69jsFOJhWfcVekJCUjLHNMaT+l4h6si2ASZE+bOJ3o/4FQQQnJyqYYwadirdO2+KbHbX5gG1sbCmAr1jWk66GWVtbSNFYIKXrDa0NYVRvODes7q3TXKWYop33HoRFZd263mA+qMY1QnQbMOasxZ12HOwUWV5KQCqSESvdfYi3uhozr+sYSSihWWoqMY1gyyjAnlDRMSyOqfDsfkZwa5xIt0A1ED8Nflm+diWEMfSpO1RdfQriffP5Zi/kMXpzHr3P4y2CeIBa5PO18vstTX4fD4fAHAAD//wMA/WXCnPMDAAA=
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUy27kIBD8FYvz4GDA2PiWpzaKsok2o+xqbzzaCYoDI8B5KJp/X+GZKDun1V4QancX1VUlfyCvngEN6HizOX8BnxNaoeyWEiVUYCJx061JO/B+aEXdU0lb2v1GK+Su4B0NqOPGNEYCZiAAc0tarNlosLUgG9JYQlteMNVDQsMHUq628OIM1CHdQ0wueDSgn87b8FreVq42U5htHcMElz5l5U0hc3Z+d7W+ucUXnfh+fnKy60yQCkDtLBqQlb0xPWW462iLuQKD5agUVopZZdUopGx2Y85niF5NdbJPXxxsyB6yGWhNWU0wlWi7QlZlVWhrlWD9vilMFpnOSn21lJfr8IFeIKKBrj4F3cEdmckdPSc9u8ke7c6s0lNazlGZHOI7WqFNDBuI2cGi0XFK8Kyn97VKTxe7nnJN529g5gz2NMw+owExgVZo0WiaVHbBV3uKp5MDn5ePOTqfnPkXVEG6uatuJ5XHEJ8PLLnbqXz5HyJfK/PoPFSXZwW74ZZLYjFvtMK8GyVWwBTWrZC818wKKAk5DT47P4c5VZc+w0NcVkIDulBTgh2/42geXQaT51j2/CXK4G0Mdja5+rKyITWpGWFlaM6bOVc/wLoIJkNZYh3nPd794QgVlJBCZY4RfK5uVX6svqn0WBpsZ1tDAWzDdEf0CG1v+7Y3nRWiF6PhrSGsaQ3nho3aSqW1ZIZyOmoOXREYSnKqJaySso522mA7tgJzYhusjBW4oVJT23R2pPRAx+pmKnN67CyhhGKpeoE50wz3jDLMCSUdU70RTcnlpyT7PPAerdAVRA/TXytfOxNDCmOu9l5XByL8mH35EVSL76/O47dF7bNgniBWxS3lfMn83qDtdrv9AwAA//8DAJ9uh65PBAAA
@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRVTW/jNhD9KwKvlbQSKZGSgD1kHWcbLPKB2OgWvQ3JYcxGIQ2KyjYN/N8LyknX2UUPvdjSSDPz5r031Atx8IhkIGf7/foJXZxITqJdQrSivKj6ohbbqh2abmh52bGu5XX/B8mJ/YLPZCCiUapWPRYMORaNrtpCMqMKrbGvq1pXtG1STbifyPBCwJYan6zC0k+/YZisd2QgX63T/lvqDbZUo591GfyIl26K4FQCc77efNne3BYXgl+vP306vjnhlAqUVpOB6L5TqqOsEIK2RQOoit4AFABMgwbD+74+plkXMTgYy0k/fMegfXQY1UBLysqqoD055ERDhARbwoTb531CstB0nuL5El4uhxfyhIEMNH8j9Fjugxrth8dJznbUH5ZfkpN98HsM0eLCyDLkOEK03mWvPVajRRdJTj6llM2sFE4TGcgFjBO+hdfu3jq8CPCI33x4uD72La/XW5KT2+D/RBVvIe7IQJhRnTZS1dD00kjUPa9awB5MI5vOaM1qZRStTcWFkLQxrNNSAuei1VJSSnJys8luR4jGh8d3gm2OGlz+Dwk2Z6u1Azmi/nGkLYR7jGQgLW8lMtNRLjlo6FlnakDWayUZb7GTQncoBKMMVKuqrla1qFvoRdsKzmjqcjltIjgNo3e4/gvVnAgmA9mGOfW7ArWzDrPLczIQXje66StdNLWEohGmLwAZFLLlfdNJpjkmE1+AHeeAK4h470Ny//X8GZNOK++idbOfp+zSRbwPi5wn491ssrOgdjaiinNIQv3OU8nb4PWsYvbdh3VVViWrWEqa436O2R1qG1DFha9X+DebH1Mop1WVoMwhoItZkj77Faakf62FbhVF1DWTopIG2053baeE5rzjRjWtqljdqqZRzEjdg5Q9U7ShRjYoOMkJJttny6Y1XS8qo6DoIFHGelOAkrLg0vTQiKpjlXjHcHYzpjxphK5oRYseOl40TLKiY5QVTUUrwaBTvIb35j4ZsSt5WTclE91CzRtvrwvTdCQn6xB8WPk5nWLD6d2lMz57yVb+cW9HDNnHLM+upqXPZ3QYYDwNrZ9gnI/reBp9s9C77AD73RLYwvSwXGz0wx1OfnzCMC2Ba4wbfXy2uCX7mNV5tqSvdqhen0C0T7j1flQ7sMceK6/xzMH4PNljpTv42x/Bf92b5f9s2l8vFfPsJu6WybIDyckXDA7HE4dcWRX85E3MXjc3e+eZE8rP7bQf4fkn5n9BwSpTa5mk/Y7+50W+m136gGTLifDNuuKvxejnXj1gyNKigHXprPw342oeo93uAoJGfeU1/lj0kJNHhGkO+Lh8opZT02FYYJzPx2W7dFd2HO2Eyjs9kYFz3pSM0/YV7n++JxpaVqzmh8Ph8A8AAAD//wMADw4oQRAHAAA=
@@ -0,0 +1,18 @@
{
"Microsoft.NET.Workload.Emscripten.Current": "10.0.111/10.0.100",
"Microsoft.NET.Workload.Emscripten.net6": "10.0.111/10.0.100",
"Microsoft.NET.Workload.Emscripten.net7": "10.0.111/10.0.100",
"Microsoft.NET.Workload.Emscripten.net8": "10.0.111/10.0.100",
"Microsoft.NET.Workload.Emscripten.net9": "10.0.111/10.0.100",
"Microsoft.NET.Sdk.Android": "36.1.69/10.0.100",
"Microsoft.NET.Sdk.iOS": "26.5.10315/10.0.100",
"Microsoft.NET.Sdk.MacCatalyst": "26.5.10315/10.0.100",
"Microsoft.NET.Sdk.macOS": "26.5.10315/10.0.100",
"Microsoft.NET.Sdk.Maui": "10.0.20/10.0.100",
"Microsoft.NET.Sdk.tvOS": "26.5.10315/10.0.100",
"Microsoft.NET.Workload.Mono.ToolChain.Current": "10.0.111/10.0.100",
"Microsoft.NET.Workload.Mono.ToolChain.net6": "10.0.111/10.0.100",
"Microsoft.NET.Workload.Mono.ToolChain.net7": "10.0.111/10.0.100",
"Microsoft.NET.Workload.Mono.ToolChain.net8": "10.0.111/10.0.100",
"Microsoft.NET.Workload.Mono.ToolChain.net9": "10.0.111/10.0.100"
}
@@ -13,41 +13,16 @@ namespace Distribution.Migrations
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_DeliveryRuns_Routes_FK_RouteId",
table: "DeliveryRuns");
DropForeignKeyIfExists(migrationBuilder, "DeliveryRuns", "FK_DeliveryRuns_Routes_FK_RouteId");
DropForeignKeyIfExists(migrationBuilder, "DeliveryRuns", "FK_DeliveryRuns_Warehouses_FK_SourceWarehouseId");
migrationBuilder.DropForeignKey(
name: "FK_DeliveryRuns_Warehouses_FK_SourceWarehouseId",
table: "DeliveryRuns");
migrationBuilder.DropForeignKey(
name: "FK_DeliveryTasks_Orders_FK_OrderId",
table: "DeliveryTasks");
migrationBuilder.DropForeignKey(
name: "FK_LoadingSheetItems_LoadingSheets_FK_LoadingSheetId",
table: "LoadingSheetItems");
migrationBuilder.DropForeignKey(
name: "FK_LoadingSheets_Warehouses_FK_SourceWarehouseId",
table: "LoadingSheets");
migrationBuilder.DropForeignKey(
name: "FK_Payments_Sales_FK_SaleId",
table: "Payments");
migrationBuilder.DropForeignKey(
name: "FK_Payments_Stores_FK_StoreId",
table: "Payments");
migrationBuilder.DropForeignKey(
name: "FK_SaleReturns_Orders_FK_OrderId",
table: "SaleReturns");
migrationBuilder.DropForeignKey(
name: "FK_SchemeRedemptions_Orders_FK_OrderId",
table: "SchemeRedemptions");
DropForeignKeyIfExists(migrationBuilder, "DeliveryTasks", "FK_DeliveryTasks_Orders_FK_OrderId");
DropForeignKeyIfExists(migrationBuilder, "LoadingSheetItems", "FK_LoadingSheetItems_LoadingSheets_FK_LoadingSheetId");
DropForeignKeyIfExists(migrationBuilder, "LoadingSheets", "FK_LoadingSheets_Warehouses_FK_SourceWarehouseId");
DropForeignKeyIfExists(migrationBuilder, "Payments", "FK_Payments_Sales_FK_SaleId");
DropForeignKeyIfExists(migrationBuilder, "Payments", "FK_Payments_Stores_FK_StoreId");
DropForeignKeyIfExists(migrationBuilder, "SaleReturns", "FK_SaleReturns_Orders_FK_OrderId");
DropForeignKeyIfExists(migrationBuilder, "SchemeRedemptions", "FK_SchemeRedemptions_Orders_FK_OrderId");
migrationBuilder.DropTable(
name: "OrderItems");
@@ -58,57 +33,20 @@ namespace Distribution.Migrations
migrationBuilder.DropTable(
name: "Sales");
migrationBuilder.DropIndex(
name: "IX_Payments_FK_SaleId",
table: "Payments");
DropIndexIfExists(migrationBuilder, "Payments", "IX_Payments_FK_SaleId");
DropIndexIfExists(migrationBuilder, "LoadingSheets", "IX_LoadingSheets_FK_SourceWarehouseId");
DropIndexIfExists(migrationBuilder, "DeliveryRuns", "IX_DeliveryRuns_FK_RouteId");
DropIndexIfExists(migrationBuilder, "DeliveryRuns", "IX_DeliveryRuns_FK_SourceWarehouseId");
migrationBuilder.DropIndex(
name: "IX_LoadingSheets_FK_SourceWarehouseId",
table: "LoadingSheets");
migrationBuilder.DropIndex(
name: "IX_DeliveryRuns_FK_RouteId",
table: "DeliveryRuns");
migrationBuilder.DropIndex(
name: "IX_DeliveryRuns_FK_SourceWarehouseId",
table: "DeliveryRuns");
migrationBuilder.DropColumn(
name: "FK_SaleId",
table: "Payments");
migrationBuilder.DropColumn(
name: "DispatchConfirmedAt",
table: "LoadingSheets");
migrationBuilder.DropColumn(
name: "DispatchConfirmedBy",
table: "LoadingSheets");
migrationBuilder.DropColumn(
name: "QuantityReceived",
table: "LoadingSheetItems");
migrationBuilder.DropColumn(
name: "QuantityResolved",
table: "LoadingSheetItems");
migrationBuilder.DropColumn(
name: "ActivatedAt",
table: "DeliveryRuns");
migrationBuilder.DropColumn(
name: "FK_RouteId",
table: "DeliveryRuns");
migrationBuilder.DropColumn(
name: "FK_SourceWarehouseId",
table: "DeliveryRuns");
migrationBuilder.DropColumn(
name: "RideType",
table: "DeliveryRuns");
DropColumnIfExists(migrationBuilder, "Payments", "FK_SaleId");
DropColumnIfExists(migrationBuilder, "LoadingSheets", "DispatchConfirmedAt");
DropColumnIfExists(migrationBuilder, "LoadingSheets", "DispatchConfirmedBy");
DropColumnIfExists(migrationBuilder, "LoadingSheetItems", "QuantityReceived");
DropColumnIfExists(migrationBuilder, "LoadingSheetItems", "QuantityResolved");
DropColumnIfExists(migrationBuilder, "DeliveryRuns", "ActivatedAt");
DropColumnIfExists(migrationBuilder, "DeliveryRuns", "FK_RouteId");
DropColumnIfExists(migrationBuilder, "DeliveryRuns", "FK_SourceWarehouseId");
DropColumnIfExists(migrationBuilder, "DeliveryRuns", "RideType");
migrationBuilder.RenameColumn(
name: "FK_OrderId",
@@ -957,5 +895,66 @@ namespace Distribution.Migrations
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
private static void DropForeignKeyIfExists(MigrationBuilder migrationBuilder, string tableName, string constraintName)
{
migrationBuilder.Sql($"""
SET @sql = IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = '{tableName}'
AND CONSTRAINT_NAME = '{constraintName}'
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
),
'ALTER TABLE `{tableName}` DROP FOREIGN KEY `{constraintName}`',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
""");
}
private static void DropIndexIfExists(MigrationBuilder migrationBuilder, string tableName, string indexName)
{
migrationBuilder.Sql($"""
SET @sql = IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '{tableName}'
AND INDEX_NAME = '{indexName}'
),
'DROP INDEX `{indexName}` ON `{tableName}`',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
""");
}
private static void DropColumnIfExists(MigrationBuilder migrationBuilder, string tableName, string columnName)
{
migrationBuilder.Sql($"""
SET @sql = IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '{tableName}'
AND COLUMN_NAME = '{columnName}'
),
'ALTER TABLE `{tableName}` DROP COLUMN `{columnName}`',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
""");
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"Urls": "http://localhost:5089",
"ConnectionStrings": {
"DefaultConnection": "Server=187.127.102.190;Port=3306;Database=distribution_db;User=kalana;Password=kalana@hexdive"
"DefaultConnection": "Server=187.127.102.190;Port=3306;Database=distribution;User=kalana;Password=kalana@hexdive"
},
"Logging": {
"LogLevel": {
@@ -684,15 +684,33 @@ appsettings.Development.json
---
## Open Items (must resolve before coding the relevant phase)
## Open Items
### Resolved during implementation
| # | Item | Resolution |
|---|---|---|
| 2 | Cascade when Confirmed ShopPO is cancelled and Invoice already exists | Cancel is rejected (400) if a linked Invoice exists. Invoice must be voided separately before the ShopPO can be cancelled. No automatic cascade. |
| 3 | Cascade when Confirmed ShopPO is cancelled and it's in a LoadingSheet | Cancel is rejected (400) if the ShopPO appears in any Planned LoadingSheet. Operator must remove it from the sheet first. |
| 4 | `FinalizeInvoiceRequest` payload — free issues field shape | Deferred to a later pass. v1 `POST /api/invoices/{id}/finalize` takes no body and locks the invoice as-is. |
| 5 | Who can confirm a ShopPO? | No role restriction in v1 — any user can confirm. Gating deferred until the auth layer is implemented. |
### Still open
| # | Item | Blocks |
|---|---|---|
| 1 | Who holds `loading.confirm` permission? | Phase 9 — ConfirmAsync auth guard |
| 2 | Cascade when Confirmed ShopPO is cancelled and Invoice already exists | Phase 7 — CancelAsync |
| 3 | Cascade when Confirmed ShopPO is cancelled and it's in a LoadingSheet | Phase 7 — CancelAsync |
| 4 | `FinalizeInvoiceRequest` payload — free issues field shape | Phase 6 — FinalizeAsync |
| 5 | Who can confirm a ShopPO? (agent, office, either?) | Phase 7 — ConfirmAsync permission |
| 1 | Who holds `loading.confirm` permission? | Permission seeding (`loading.populate`, `loading.confirm`) — explicitly noted as **Pending** in the API reference. Must decide role before seeding. |
### New items — identified during frontend refactor (2026-09-17)
| # | Item | Area |
|---|---|---|
| 6 | `order-return-edit-dialog.tsx` — file not verified; client imports it but it was not reviewed during the refactor pass. Needs to be checked for old `orderId`/`OrderResponse` references. | Returns |
| 7 | `sales-visits/` components — any components in this section that interact with sales/orders need auditing against the new ShopPO/Invoice types. | Sales Visits |
| 8 | SysConfig management page not built — the backend has a `SysConfig` model and the `InvoiceCreationMode` config key is read by `ShopPOController`. A UI to view and toggle system config is missing. | Config |
| 9 | InvoiceCreationMode (ModeA vs ModeB) UI branching — the ShopPO confirm button calls `confirm` or `confirmAndCreateInvoice` depending on mode, but the mode is not currently read from the backend. The UI should fetch `GET /api/sys-config` and gate the action accordingly. | Sales / Invoice |
| 10 | Loading Sheet item population UI — `PUT /api/loading-sheets/{id}/items` exists in the API layer but no UI for populating/editing items on a Planned sheet has been built. | Dispatch |
| 11 | `PaymentMethod` enum is missing `Cheque` — per `invoice_collection.md`, the final design includes `Cash`, `Cheque`, `Credit`. The frontend only has `Cash` and `Credit`. Cheque-specific fields (`ChequeNumber`, `ChequeDate`) and `EntryMode` (`RealTime`, `DayEndManual`) are also unimplemented. | Payments |
---
@@ -0,0 +1,978 @@
# Frontend API Reference — ERP Distribution (Post-Refactor)
> Last updated: 2026-09-17
> Base URL: `https://<host>/api`
> All responses are JSON. Enum fields are serialized as **strings** (e.g. `"Pending"`, `"ModeB"`).
> All `Guid` fields are UUID v4 strings. All `DateTime` fields are ISO 8601.
---
## Table of Contents
1. [Pagination](#pagination)
2. [Enums Reference](#enums-reference)
3. [ShopPO](#shoppo)
4. [Invoice](#invoice)
5. [Payment](#payment)
6. [Ride (DeliveryRun)](#ride-deliveryrun)
7. [Loading Sheet](#loading-sheet)
8. [Delivery Tasks](#delivery-tasks)
9. [Returns](#returns)
10. [Schemes](#schemes)
11. [System Config](#system-config)
12. [Key Relationships & Flows](#key-relationships--flows)
---
## Pagination
All list endpoints return a `PagedResponse<T>`:
```json
{
"page": 1,
"pageSize": 20,
"total": 84,
"items": [ /* T[] */ ]
}
```
Default page size is `20`. Pass `?page=2&pageSize=50` to paginate.
---
## Enums Reference
| Enum | Values |
|------|--------|
| `ShopPOStatus` | `Pending`, `PartiallyFulfilled`, `Fulfilled`, `Cancelled` |
| `InvoiceStatus` | `Draft`, `Printed`, `Finalized` |
| `PaymentStatus` | `Pending`, `PartiallyPaid`, `Paid` |
| `PaymentMethod` | `Cash`, `Credit` |
| `RideStatus` | `Open`, `Cancelled`, `Closed` |
| `LoadingSheetStatus` | `Planned`, `Confirmed` |
| `DeliveryTaskStatus` | `Pending`, `Delivered`, `PartiallyDelivered`, `Failed` |
| `ReturnMode` | `CreditDeduction`, `ItemExchange` |
| `ReturnReasonCode` | `Expired`, `Damaged`, `OrderError`, `Other` |
| `CancelRequestType` | `VehicleBreakdown`, `DriverUnavailable`, `RouteIssue`, `Other` |
| `InvoiceCreationMode` | `ModeA`, `ModeB` |
| `SchemeRuleType` | `SlabDiscount`, `FlatPercent`, `BuyXGetY`, `FreeBundle` |
---
## ShopPO
A ShopPO is a shop-facing purchase order raised by a sales agent on behalf of a store.
### Status Flow
```
Pending → PartiallyFulfilled → Fulfilled
↘ Cancelled
```
### `GET /api/shop-pos`
List all ShopPOs with optional filters.
**Query params:**
| Param | Type | Description |
|-------|------|-------------|
| `agentId` | `Guid?` | Filter by the agent who raised the PO |
| `routeId` | `Guid?` | Filter by the store's route |
| `shopId` | `Guid?` | Filter by store |
| `confirmed` | `bool?` | `true` = confirmed only, `false` = unconfirmed only |
| `page` | `int` | Default `1` |
| `pageSize` | `int` | Default `20` |
**Response:** `PagedResponse<ShopPOResponse>`
```json
{
"page": 1,
"pageSize": 20,
"total": 12,
"items": [
{
"id": "uuid",
"storeId": "uuid",
"storeName": "Colombo Mart",
"raisedById": "uuid",
"invoiceId": "uuid | null",
"totalAmount": 15000.00,
"status": "Pending",
"confirmedAt": "2026-09-17T10:00:00 | null",
"confirmedBy": "uuid | null",
"fulfilledAt": "2026-09-17T14:00:00 | null",
"items": [
{
"id": "uuid",
"itemId": "uuid",
"itemName": "Milo 400g",
"quantity": 10,
"quantityFulfilled": 0,
"unitPrice": 850.00,
"isActive": true
}
],
"isActive": true,
"createdAt": "2026-09-17T08:00:00"
}
]
}
```
### `GET /api/shop-pos/{id}`
**Response:** `ShopPOResponse` (same shape as above, single object) or `404`.
---
## Invoice
An Invoice is the billing document generated from a confirmed ShopPO. There is at most one Invoice per ShopPO.
**InvoiceCreationMode** (system config) controls whether the invoice is:
- **ModeA** — auto-created when the ShopPO is confirmed
- **ModeB** — created as a separate explicit step (default)
### Status Flow
```
Draft → Printed → Finalized
```
Once Finalized, the invoice is locked. Payments can be collected at any status.
### `GET /api/invoices`
**Query params:**
| Param | Type | Description |
|-------|------|-------------|
| `agentId` | `Guid?` | Agent who raised the ShopPO |
| `routeId` | `Guid?` | Store's route |
| `shopId` | `Guid?` | Store |
| `status` | `InvoiceStatus?` | `Draft`, `Printed`, `Finalized` |
| `page` | `int` | Default `1` |
| `pageSize` | `int` | Default `20` |
**Response:** `PagedResponse<InvoiceResponse>`
```json
{
"page": 1,
"pageSize": 20,
"total": 5,
"items": [
{
"id": "uuid",
"shopPOId": "uuid",
"storeName": "Colombo Mart",
"status": "Draft",
"paymentStatus": "Pending",
"createdBy": "uuid",
"printedAt": null,
"printedBy": null,
"finalizedAt": null,
"finalizedBy": null,
"totalAmount": 15000.00,
"isActive": true,
"createdAt": "2026-09-17T08:30:00"
}
]
}
```
### `GET /api/invoices/{id}`
**Response:** `InvoiceResponse` (single) or `404`.
> **Note:** The detail response from the repository also loads `Payments`. The current `InvoiceResponse` DTO does not yet expose payments — use `GET /api/invoices/{id}/payments` for that.
---
## Payment
Payments are collected against an Invoice. Multiple payments can be made on one invoice (e.g. partial cash collection).
### `GET /api/invoices/{invoiceId}/payments`
Returns all payments for the invoice.
**Response:** `PaymentResponse[]`
```json
[
{
"id": "uuid",
"invoiceId": "uuid",
"amount": 5000.00,
"method": "Cash",
"collectedAt": "2026-09-17T15:00:00",
"collectedBy": "uuid",
"createdAt": "2026-09-17T15:00:00"
}
]
```
Returns `404` if the invoice does not exist.
### `POST /api/invoices/{invoiceId}/payments`
Record a payment against an invoice.
**Request body:**
```json
{
"amount": 5000.00,
"method": "Cash",
"collectedByUserId": "uuid | null"
}
```
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `amount` | `decimal` | Yes | Must be > 0 |
| `method` | `PaymentMethod` | Yes | `Cash` or `Credit` |
| `collectedByUserId` | `Guid?` | No | Defaults to authenticated user |
**Response:** `PaymentResponse` or `404` if invoice not found, `400` on business rule violation.
---
## Ride (DeliveryRun)
A Ride represents a vehicle dispatch for a day. The driver moves along a route making deliveries.
### Status Flow
```
Open → Closed
↘ Cancelled
```
### `GET /api/rides`
**Query params:**
| Param | Type | Description |
|-------|------|-------------|
| `status` | `RideStatus?` | `Open`, `Cancelled`, `Closed` |
| `driverProfileId` | `Guid?` | Filter by driver |
| `vehicleId` | `Guid?` | Filter by vehicle |
| `page` | `int` | Default `1` |
| `pageSize` | `int` | Default `20` |
**Response:** `PagedResponse<RideResponse>`
```json
{
"items": [
{
"id": "uuid",
"status": "Open",
"vehicleId": "uuid",
"vehicleNumberPlate": "WP CAB 1234",
"driverProfileId": "uuid",
"driverFullName": "Kamal Perera",
"meterstart": 12450,
"meterend": 0,
"startedAt": null,
"closedAt": null,
"createdAt": "2026-09-17T06:00:00"
}
]
}
```
### `GET /api/rides/{id}`
**Response:** `RideResponse` or `404`.
### `POST /api/rides`
Create a new ride.
**Request body:**
```json
{
"vehicleId": "uuid",
"driverProfileId": "uuid",
"meterstart": 12450
}
```
**Response:** `201 Created` with `RideResponse`. `400` on validation error.
### `PUT /api/rides/{id}/driver`
Re-assign the driver on an Open ride.
**Request body:**
```json
{
"driverProfileId": "uuid"
}
```
**Response:** `RideResponse`. `404` if ride not found. `400` if ride is not Open.
### `POST /api/rides/{id}/close`
Close a ride and record the ending odometer.
**Request body:**
```json
{
"meterend": 12890
}
```
**Response:** `RideResponse`. `400` if ride is not Open.
### `POST /api/rides/{id}/cancel`
Cancel a ride.
**Request body:**
```json
{
"typeCode": "VehicleBreakdown",
"reason": "Engine overheating"
}
```
| Field | Type | Notes |
|-------|------|-------|
| `typeCode` | `CancelRequestType` | `VehicleBreakdown`, `DriverUnavailable`, `RouteIssue`, `Other` |
| `reason` | `string` | Free text reason |
**Response:** `RideResponse`. `400` if ride is not Open.
---
## Loading Sheet
A Loading Sheet records the items loaded onto a vehicle before a ride. It belongs to a Ride.
### Status Flow
```
Planned → Confirmed
```
Items can only be edited while the sheet is `Planned`. Confirming the sheet commits stock movements (Warehouse → Vehicle).
### `GET /api/loading-sheets`
**Query params:**
| Param | Type | Description |
|-------|------|-------------|
| `rideId` | `Guid?` | Filter by ride |
| `status` | `LoadingSheetStatus?` | `Planned`, `Confirmed` |
| `page` | `int` | Default `1` |
| `pageSize` | `int` | Default `20` |
**Response:** `PagedResponse<LoadingSheetResponse>`
```json
{
"items": [
{
"id": "uuid",
"fK_RideId": "uuid",
"status": "Planned",
"populatedBy": "uuid",
"populatedAt": "2026-09-17T07:00:00",
"confirmedAt": null,
"confirmedBy": null,
"createdAt": "2026-09-17T07:00:00",
"items": [
{
"id": "uuid",
"fK_ItemId": "uuid",
"itemName": "Milo 400g",
"fK_WarehouseId": "uuid",
"warehouseName": "Colombo Warehouse",
"quantityDispatched": 24.0,
"fK_ShopPOItemId": "uuid | null"
}
],
"orders": [
{ "shopPOId": "uuid" }
]
}
]
}
```
### `GET /api/loading-sheets/{id}`
**Response:** `LoadingSheetResponse` or `404`.
### `POST /api/loading-sheets`
Create a new loading sheet for a ride. The ride must exist. Only one `Planned` sheet per ride is allowed.
**Request body:**
```json
{
"fK_RideId": "uuid"
}
```
**Response:** `201 Created` with `LoadingSheetResponse`. `400` if ride already has a planned sheet.
### `PUT /api/loading-sheets/{id}/items`
Replace all items on a `Planned` loading sheet. This is a full replace — any items not included are removed.
**Request body:**
```json
{
"items": [
{
"fK_ItemId": "uuid",
"fK_WarehouseId": "uuid",
"quantity": 24.0,
"fK_ShopPOItemId": "uuid | null"
}
]
}
```
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `fK_ItemId` | `Guid` | Yes | The item being loaded |
| `fK_WarehouseId` | `Guid` | Yes | Source warehouse for this item |
| `quantity` | `decimal` | Yes | Units to load |
| `fK_ShopPOItemId` | `Guid?` | No | Links the load line to a specific ShopPO line item |
**Response:** `LoadingSheetResponse`. `400` if sheet is not `Planned`.
### `POST /api/loading-sheets/{id}/confirm`
Confirm the loading sheet. Commits stock: moves each item from its specified warehouse to the vehicle. Sheet becomes `Confirmed` and can no longer be edited.
**Request body:** _(none)_
**Response:** `LoadingSheetResponse`. `400` if already confirmed.
---
## Delivery Tasks
Delivery Tasks are the per-stop delivery jobs attached to a Ride. Each task maps to one ShopPO.
> **Note:** Auto-generation of delivery tasks from route/ShopPO assignments is deferred and not yet implemented. Tasks must be created manually or via a future dispatch endpoint.
### Status Flow
```
Pending → Delivered
↘ PartiallyDelivered
↘ Failed
```
### `GET /api/rides/{rideId}/tasks`
List all delivery tasks for a ride, ordered by `Sequence`.
**Response:** `DeliveryTaskResponse[]`
```json
[
{
"id": "uuid",
"fK_RideId": "uuid",
"fK_ShopPOId": "uuid",
"storeId": "uuid",
"storeName": "Galle Mart",
"sequence": 1,
"status": "Pending",
"customerConfirmed": false,
"confirmedAt": null,
"note": null,
"items": [
{
"itemId": "uuid",
"itemName": "Milo 400g",
"quantity": 10,
"quantityFulfilled": 0,
"unitPrice": 850.00
}
],
"createdAt": "2026-09-17T08:00:00"
}
]
```
### `GET /api/delivery-tasks/{id}`
**Response:** `DeliveryTaskResponse` or `404`.
### `PUT /api/delivery-tasks/{id}/quantities`
Set the fulfilled quantity for each item in the task. Call this before confirming.
**Request body:**
```json
{
"items": [
{ "itemId": "uuid", "quantityFulfilled": 8 }
]
}
```
**Response:** `DeliveryTaskResponse`. `400` if task is already closed.
### `POST /api/delivery-tasks/{id}/confirm`
Mark the task as Delivered or PartiallyDelivered based on quantities set. Updates ShopPO fulfillment, creates stock movement (Vehicle → External), and records payment if applicable.
**Request body:** _(none)_
**Response:** `DeliveryTaskResponse`. `400` if task is not `Pending`.
### `POST /api/delivery-tasks/{id}/fail`
Mark the task as Failed with a reason note.
**Request body:**
```json
{
"note": "Store was closed"
}
```
**Response:** `DeliveryTaskResponse`. `400` if task is not `Pending`.
---
## Returns
Returns are recorded against a ShopPO during the same store visit. Effects (credit deduction or item exchange) are applied immediately — no approval step.
> **Note:** The Create endpoint (`POST /api/shop-pos/{id}/returns`) is currently stubbed (`501 Not Implemented`) while the return flow is being redesigned. `GET` and `PATCH` endpoints are fully operational.
### `GET /api/returns`
List all returns.
**Response:** `SaleReturnResponse[]`
```json
[
{
"id": "uuid",
"shopPOId": "uuid",
"mode": "CreditDeduction",
"totalCreditAmount": 1700.00,
"orderTotalAmountAfter": 13300.00,
"isActive": true,
"createdAt": "2026-09-17T11:00:00",
"items": [
{
"id": "uuid",
"itemId": "uuid",
"sku": "MILO400",
"itemName": "Milo 400g",
"quantity": 2.0,
"reasonCode": "Damaged",
"unitPrice": 850.00
}
]
}
]
```
### `GET /api/returns/{id}`
**Response:** `SaleReturnResponse` or `404`.
### `GET /api/shop-pos/{id}/returns`
List all returns for a specific ShopPO.
**Response:** `SaleReturnResponse[]`
### `POST /api/shop-pos/{id}/returns`
> **501 Not Implemented** — return flow under redesign.
**Request body (for future reference):**
```json
{
"mode": "CreditDeduction",
"items": [
{ "itemId": "uuid", "quantity": 2.0, "reasonCode": "Damaged" }
],
"createdByUserId": "uuid"
}
```
| `mode` | Effect |
|--------|--------|
| `CreditDeduction` | `totalCreditAmount` is netted off the ShopPO's `TotalAmount` |
| `ItemExchange` | Items are swapped; no monetary credit applied |
### `PATCH /api/returns/{id}/reasons`
Update the reason code on individual return line items. Only reason codes can be edited post-creation.
**Request body:**
```json
{
"items": [
{ "itemId": "uuid", "reasonCode": "Expired" }
]
}
```
**Response:** `SaleReturnResponse`. `409` if return is no longer active.
---
## Schemes
Schemes are promotional rules applied to ShopPOs (formerly Orders).
### Scheme Rule Types
| Type | Description |
|------|-------------|
| `SlabDiscount` | Tiered % discount based on order amount. Config: `SlabDiscountConfig` |
| `FlatPercent` | Flat % discount on order total. Config: `FlatPercentConfig` |
| `BuyXGetY` | Buy N of trigger item → get free payout item |
| `FreeBundle` | Buy a bundle of items → get a free payout item |
### `GET /api/schemes`
List all schemes (paginated).
**Response:** `PagedResponse<SchemeResponse>`
```json
{
"items": [
{
"id": "uuid",
"name": "Q3 Volume Boost",
"ruleType": "SlabDiscount",
"ruleConfig": "{\"Tiers\":[{\"MinOrderAmount\":10000,\"DiscountPercent\":5}]}",
"triggerItemId": null,
"triggerItemName": null,
"triggerQty": null,
"freeItemId": null,
"freeItemName": null,
"freeQty": null,
"bundleComponents": [],
"regionId": "uuid | null",
"regionName": "Western Province | null",
"eligibleSegment": null,
"dateRangeFrom": "2026-07-01T00:00:00",
"dateRangeTo": "2026-09-30T00:00:00",
"agentLimitExempt": false,
"baselineExpectedSales": 500000.00,
"isActive": true,
"createdAt": "2026-06-15T09:00:00"
}
]
}
```
### `GET /api/schemes/{id}`
**Response:** `SchemeResponse` or `404`.
### `POST /api/schemes`
Create a scheme. For discount-only schemes (`SlabDiscount`, `FlatPercent`), free-goods fields can be omitted. For `BuyXGetY` / `FreeBundle`, you can supply the free-goods config inline here or set it later via `PUT .../free-goods-rule`.
**Request body:**
```json
{
"name": "Q3 Volume Boost",
"ruleType": "SlabDiscount",
"ruleConfig": "{\"Tiers\":[{\"MinOrderAmount\":10000,\"DiscountPercent\":5}]}",
"regionId": null,
"eligibleSegment": null,
"dateRangeFrom": "2026-07-01T00:00:00",
"dateRangeTo": "2026-09-30T00:00:00",
"agentLimitExempt": false,
"baselineExpectedSales": 500000.00
}
```
#### `ruleConfig` shapes
**SlabDiscount:**
```json
{ "Tiers": [ { "MinOrderAmount": 10000, "DiscountPercent": 5 }, { "MinOrderAmount": 20000, "DiscountPercent": 8 } ] }
```
**FlatPercent:**
```json
{ "DiscountPercent": 3.5 }
```
**BuyXGetY / FreeBundle:** `ruleConfig` is `null` — the payout is set via `PUT .../free-goods-rule`.
### `PUT /api/schemes/{id}`
Update scheme metadata. Cannot change `ruleType`.
### `DELETE /api/schemes/{id}`
Soft-delete (sets `IsActive = false`).
### `PUT /api/schemes/{id}/free-goods-rule`
Set the free-goods payout for `BuyXGetY` or `FreeBundle` schemes.
**Request body:**
```json
{
"triggerItemId": "uuid | null",
"triggerQty": 6,
"freeItemId": "uuid",
"freeQty": 1.0
}
```
- `BuyXGetY`: `triggerItemId` + `triggerQty` required (what store must buy).
- `FreeBundle`: `triggerItemId` / `triggerQty` ignored — trigger is the bundle components.
### `PUT /api/schemes/{id}/bundle-components/{itemId}`
Add or update a component in a `FreeBundle` scheme.
**Request body:**
```json
{ "qty": 4 }
```
### `DELETE /api/schemes/{id}/bundle-components/{itemId}`
Remove a bundle component.
### `GET /api/shop-pos/{shopPOId}/scheme-eligibility`
Check which active schemes the ShopPO qualifies for.
**Response:** `SchemeEligibilityResponse[]`
```json
[
{
"schemeId": "uuid",
"schemeName": "Q3 Volume Boost",
"ruleType": "SlabDiscount",
"eligible": true,
"reason": null,
"discountAmount": 750.00,
"freeItemId": null,
"freeQty": null
}
]
```
### `POST /api/shop-pos/{shopPOId}/apply-scheme`
Apply a scheme to the ShopPO (create a redemption record and apply discount/free goods).
**Request body:**
```json
{ "schemeId": "uuid" }
```
**Response:** `SchemeRedemptionResponse`
```json
{
"id": "uuid",
"schemeId": "uuid",
"shopPOId": "uuid",
"discountAmount": 750.00,
"freeItemId": null,
"freeItemName": null,
"freeQty": null,
"freeItemCostValue": null,
"redeemedAt": "2026-09-17T09:30:00"
}
```
### `GET /api/schemes/{id}/spend`
**Response:** `SchemeSpendResponse`
```json
{
"schemeId": "uuid",
"totalDiscountSpend": 45000.00,
"totalFreeGoodsCostSpend": 12000.00,
"totalSpend": 57000.00,
"redemptionCount": 38
}
```
### `GET /api/schemes/{id}/roi`
**Response:** `SchemeROIResponse`
```json
{
"schemeId": "uuid",
"baselineExpectedSales": 500000.00,
"actualSales": 620000.00,
"incrementalSales": 120000.00,
"totalSpend": 57000.00,
"roi": 1.105
}
```
---
## System Config
Key-value store for system-wide configuration. All keys and values are strings. Only known keys have typed endpoints with validation.
### `GET /api/sys-config`
Return all config entries.
**Response:** `SysConfigResponse[]`
```json
[
{
"key": "InvoiceCreationMode",
"value": "ModeB",
"updatedAt": "2026-09-17T00:00:00",
"updatedBy": null
}
]
```
### `GET /api/sys-config/invoice-creation-mode`
**Response:**
```json
{ "mode": "ModeB" }
```
### `PUT /api/sys-config/invoice-creation-mode`
**Request body:**
```json
{ "mode": "ModeA" }
```
| Value | Behaviour |
|-------|-----------|
| `ModeA` | Invoice is auto-created when a ShopPO is confirmed |
| `ModeB` | Invoice must be created as a separate explicit step (default) |
**Response:** `SysConfigResponse`. `400` if value is not a valid `InvoiceCreationMode`.
### `GET /api/sys-config/{key}`
Generic getter by key string. `404` if key not set.
### `PUT /api/sys-config/{key}`
Generic raw string setter. Requires admin-level auth.
**Request body:**
```json
{ "value": "SomeValue" }
```
**Response:** `SysConfigResponse`. `400` if value fails validation for a known key.
---
## Key Relationships & Flows
### Core Entity Chain
```
Store ──< ShopPO ──< ShopPOItem
│
└──► Invoice ──< Payment
│
└──► SaleReturn ──< SaleReturnItem
│
└──► SchemeRedemption
Ride (DeliveryRun) ──► LoadingSheet ──< LoadingSheetItem
└──< DeliveryTask ──► ShopPO
```
### ShopPO → Invoice Dependency
- `ShopPO.invoiceId` is `null` until an invoice is created.
- In **ModeA**, the invoice is created automatically when the ShopPO is confirmed.
- In **ModeB**, the invoice must be created via a dedicated endpoint (not yet exposed — planned).
- An invoice can only exist for a confirmed ShopPO.
### Payment → Invoice Dependency
- Payments are always attached to an **Invoice**, never directly to a ShopPO.
- `Invoice.paymentStatus` is computed:
- `Pending` — no payments
- `PartiallyPaid` — payments collected but total < invoice amount
- `Paid` — fully settled
### Loading Sheet → Stock
- Confirming a loading sheet deducts each item from its **source warehouse** (`fK_WarehouseId` per line) and credits the **vehicle** stock location.
- Each loading sheet line can optionally link to a `fK_ShopPOItemId` for traceability.
### Delivery Task → ShopPO Fulfillment
- Confirming a delivery task updates `ShopPOItem.quantityFulfilled` and transitions the ShopPO status:
- All items fully fulfilled → `Fulfilled`
- Some items partially fulfilled → `PartiallyFulfilled`
### Returns → ShopPO
- Returns always reference a `shopPOId`.
- `CreditDeduction` mode: `SaleReturn.totalCreditAmount` is netted off `ShopPO.totalAmount` immediately.
- `ItemExchange` mode: no monetary impact; items are swapped in stock.
- Only the `reasonCode` on each return line can be edited after creation.
---
## What Is Not Yet Implemented
| Feature | Status | Notes |
|---------|--------|-------|
| ShopPO confirm endpoint | Pending | `PUT /api/shop-pos/{id}/confirm` — triggers InvoiceCreationMode check |
| Invoice create (ModeB) | Pending | `POST /api/invoices` with `{ shopPOId }` |
| Invoice print | Pending | `POST /api/invoices/{id}/print` |
| Invoice finalize | Pending | `POST /api/invoices/{id}/finalize` |
| Delivery task auto-generation | Deferred | Route-based dispatch not yet implemented |
| Returns create | Redesign | `POST /api/shop-pos/{id}/returns` returns `501` |
| Permission seeding | Pending | `loading.populate`, `loading.confirm` scopes |