Compare commits
6 commits
ceb26c6524
...
0059f03e87
| Author | SHA1 | Date | |
|---|---|---|---|
| 0059f03e87 | |||
| 6cf92c1db7 | |||
| 7248225cb6 | |||
| 9a865f94d9 | |||
| b8b501b12a | |||
| aa2a97bbad |
12 changed files with 1353 additions and 155 deletions
322
backend/package-lock.json
generated
Normal file
322
backend/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
{
|
||||
"name": "rocks-2many-backend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
|
||||
},
|
||||
"busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"requires": {
|
||||
"streamsearch": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"concat-stream": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^2.2.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
|
||||
},
|
||||
"es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
|
||||
},
|
||||
"es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"requires": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
|
||||
},
|
||||
"has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"requires": {
|
||||
"has-symbols": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||
},
|
||||
"math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"requires": {
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.6"
|
||||
}
|
||||
},
|
||||
"multer": {
|
||||
"version": "1.4.5-lts.2",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
|
||||
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
|
||||
"requires": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^1.0.0",
|
||||
"concat-stream": "^1.5.2",
|
||||
"mkdirp": "^0.5.4",
|
||||
"object-assign": "^4.1.1",
|
||||
"type-is": "^1.6.4",
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"requires": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
}
|
||||
},
|
||||
"typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,15 +9,16 @@
|
|||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"stripe": "^12.0.0",
|
||||
"nodemailer": "^6.9.1",
|
||||
"pg": "^8.10.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"stripe": "^12.0.0",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,30 @@ const config = {
|
|||
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET || ''
|
||||
},
|
||||
|
||||
// Shipping configuration
|
||||
shipping: {
|
||||
enabled: process.env.SHIPPING_ENABLED === 'true',
|
||||
easypostEnabled: process.env.EASYPOST_ENABLED === 'true',
|
||||
easypostApiKey: process.env.EASYPOST_API_KEY || '',
|
||||
flatRate: parseFloat(process.env.SHIPPING_FLAT_RATE || '10.00'),
|
||||
freeThreshold: parseFloat(process.env.SHIPPING_FREE_THRESHOLD || '50.00'),
|
||||
originAddress: {
|
||||
street: process.env.SHIPPING_ORIGIN_STREET || '123 Main St',
|
||||
city: process.env.SHIPPING_ORIGIN_CITY || 'Vancouver',
|
||||
state: process.env.SHIPPING_ORIGIN_STATE || 'BC',
|
||||
zip: process.env.SHIPPING_ORIGIN_ZIP || 'V6K 1V6',
|
||||
country: process.env.SHIPPING_ORIGIN_COUNTRY || 'CA'
|
||||
},
|
||||
defaultPackage: {
|
||||
length: parseFloat(process.env.SHIPPING_DEFAULT_PACKAGE_LENGTH || '15'),
|
||||
width: parseFloat(process.env.SHIPPING_DEFAULT_PACKAGE_WIDTH || '12'),
|
||||
height: parseFloat(process.env.SHIPPING_DEFAULT_PACKAGE_HEIGHT || '10'),
|
||||
unit: process.env.SHIPPING_DEFAULT_PACKAGE_UNIT || 'cm',
|
||||
weightUnit: process.env.SHIPPING_DEFAULT_WEIGHT_UNIT || 'g'
|
||||
},
|
||||
carriersAllowed: (process.env.SHIPPING_CARRIERS_ALLOWED || 'USPS,UPS,FedEx,DHL,Canada Post,Purolator').split(',')
|
||||
},
|
||||
|
||||
// Site configuration (domain and protocol based on environment)
|
||||
site: {
|
||||
domain: process.env.ENVIRONMENT === 'prod' ? 'rocks.2many.ca' : 'localhost:3000',
|
||||
|
|
@ -78,6 +102,50 @@ config.updateFromDatabase = (settings) => {
|
|||
if (stripeWebhook && stripeWebhook.value) config.payment.stripeWebhookSecret = stripeWebhook.value;
|
||||
}
|
||||
|
||||
// Update shipping settings if they exist in DB
|
||||
const shippingSettings = settings.filter(s => s.category === 'shipping');
|
||||
if (shippingSettings.length > 0) {
|
||||
const shippingEnabled = shippingSettings.find(s => s.key === 'shipping_enabled');
|
||||
const easypostEnabled = shippingSettings.find(s => s.key === 'easypost_enabled');
|
||||
const easypostApiKey = shippingSettings.find(s => s.key === 'easypost_api_key');
|
||||
const flatRate = shippingSettings.find(s => s.key === 'shipping_flat_rate');
|
||||
const freeThreshold = shippingSettings.find(s => s.key === 'shipping_free_threshold');
|
||||
const originStreet = shippingSettings.find(s => s.key === 'shipping_origin_street');
|
||||
const originCity = shippingSettings.find(s => s.key === 'shipping_origin_city');
|
||||
const originState = shippingSettings.find(s => s.key === 'shipping_origin_state');
|
||||
const originZip = shippingSettings.find(s => s.key === 'shipping_origin_zip');
|
||||
const originCountry = shippingSettings.find(s => s.key === 'shipping_origin_country');
|
||||
const packageLength = shippingSettings.find(s => s.key === 'shipping_default_package_length');
|
||||
const packageWidth = shippingSettings.find(s => s.key === 'shipping_default_package_width');
|
||||
const packageHeight = shippingSettings.find(s => s.key === 'shipping_default_package_height');
|
||||
const packageUnit = shippingSettings.find(s => s.key === 'shipping_default_package_unit');
|
||||
const weightUnit = shippingSettings.find(s => s.key === 'shipping_default_weight_unit');
|
||||
const carriersAllowed = shippingSettings.find(s => s.key === 'shipping_carriers_allowed');
|
||||
|
||||
if (shippingEnabled && shippingEnabled.value) config.shipping.enabled = shippingEnabled.value === 'true';
|
||||
if (easypostEnabled && easypostEnabled.value) config.shipping.easypostEnabled = easypostEnabled.value === 'true';
|
||||
if (easypostApiKey && easypostApiKey.value) config.shipping.easypostApiKey = easypostApiKey.value;
|
||||
if (flatRate && flatRate.value) config.shipping.flatRate = parseFloat(flatRate.value);
|
||||
if (freeThreshold && freeThreshold.value) config.shipping.freeThreshold = parseFloat(freeThreshold.value);
|
||||
|
||||
// Update origin address
|
||||
if (originStreet && originStreet.value) config.shipping.originAddress.street = originStreet.value;
|
||||
if (originCity && originCity.value) config.shipping.originAddress.city = originCity.value;
|
||||
if (originState && originState.value) config.shipping.originAddress.state = originState.value;
|
||||
if (originZip && originZip.value) config.shipping.originAddress.zip = originZip.value;
|
||||
if (originCountry && originCountry.value) config.shipping.originAddress.country = originCountry.value;
|
||||
|
||||
// Update default package
|
||||
if (packageLength && packageLength.value) config.shipping.defaultPackage.length = parseFloat(packageLength.value);
|
||||
if (packageWidth && packageWidth.value) config.shipping.defaultPackage.width = parseFloat(packageWidth.value);
|
||||
if (packageHeight && packageHeight.value) config.shipping.defaultPackage.height = parseFloat(packageHeight.value);
|
||||
if (packageUnit && packageUnit.value) config.shipping.defaultPackage.unit = packageUnit.value;
|
||||
if (weightUnit && weightUnit.value) config.shipping.defaultPackage.weightUnit = weightUnit.value;
|
||||
|
||||
// Update carriers allowed
|
||||
if (carriersAllowed && carriersAllowed.value) config.shipping.carriersAllowed = carriersAllowed.value.split(',');
|
||||
}
|
||||
|
||||
// Update site settings if they exist in DB
|
||||
const siteSettings = settings.filter(s => s.category === 'site');
|
||||
if (siteSettings.length > 0) {
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ const productRoutes = require('./routes/products');
|
|||
const authRoutes = require('./routes/auth');
|
||||
const cartRoutes = require('./routes/cart');
|
||||
const productAdminRoutes = require('./routes/productAdmin');
|
||||
const categoryAdminRoutes = require('./routes/categoryAdmin'); // Add category admin routes
|
||||
const categoryAdminRoutes = require('./routes/categoryAdmin');
|
||||
const usersAdminRoutes = require('./routes/userAdmin');
|
||||
const ordersAdminRoutes = require('./routes/orderAdmin');
|
||||
const userOrdersRoutes = require('./routes/userOrders');
|
||||
const shippingRoutes = require('./routes/shipping');
|
||||
|
||||
// Create Express app
|
||||
const app = express();
|
||||
const port = config.port || 4000;
|
||||
|
|
@ -240,8 +242,6 @@ app.delete('/api/image/product/:filename', adminAuthMiddleware(pool, query), (re
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Use routes
|
||||
app.use('/api/admin/settings', settingsAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
|
||||
app.use('/api/products', productRoutes(pool, query));
|
||||
|
|
@ -249,8 +249,9 @@ app.use('/api/auth', authRoutes(pool, query));
|
|||
app.use('/api/user/orders', userOrdersRoutes(pool, query, authMiddleware(pool, query)));
|
||||
app.use('/api/cart', cartRoutes(pool, query, authMiddleware(pool, query)));
|
||||
app.use('/api/admin/products', productAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
|
||||
app.use('/api/admin/categories', categoryAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
|
||||
app.use('/api/shipping', shippingRoutes(pool, query, authMiddleware(pool, query)));
|
||||
|
||||
app.use('/api/admin/categories', categoryAdminRoutes(pool, query, adminAuthMiddleware(pool, query))); // Add category admin routes
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
const express = require('express');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const router = express.Router();
|
||||
const shippingService = require('../services/shippingService.js');
|
||||
const config = require('../config');
|
||||
|
||||
module.exports = (pool, query, authMiddleware) => {
|
||||
|
||||
|
|
@ -37,6 +39,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
`SELECT ci.id, ci.quantity, ci.added_at,
|
||||
p.id AS product_id, p.name, p.description, p.price,
|
||||
p.category_id, pc.name AS category_name,
|
||||
p.weight_grams, p.length_cm, p.width_cm, p.height_cm,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
|
|
@ -53,7 +56,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
JOIN products p ON ci.product_id = p.id
|
||||
JOIN product_categories pc ON p.category_id = pc.id
|
||||
WHERE ci.cart_id = $1
|
||||
GROUP BY ci.id, ci.quantity, ci.added_at, p.id, p.name, p.description, p.price, p.category_id, pc.name`,
|
||||
GROUP BY ci.id, ci.quantity, ci.added_at, p.id, p.name, p.description, p.price, p.category_id, pc.name, p.weight_grams, p.length_cm, p.width_cm, p.height_cm`,
|
||||
[cartId]
|
||||
);
|
||||
|
||||
|
|
@ -72,16 +75,28 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
});
|
||||
|
||||
// Calculate total
|
||||
const total = processedItems.reduce((sum, item) => {
|
||||
const subtotal = processedItems.reduce((sum, item) => {
|
||||
return sum + (parseFloat(item.price) * item.quantity);
|
||||
}, 0);
|
||||
|
||||
// Initialize shipping
|
||||
const shipping = {
|
||||
rates: []
|
||||
};
|
||||
|
||||
// Calculate basic flat rate shipping
|
||||
if (config.shipping.enabled) {
|
||||
shipping.rates = await shippingService.getFlatRateShipping(subtotal);
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: cartId,
|
||||
userId,
|
||||
items: processedItems,
|
||||
itemCount: processedItems.length,
|
||||
total
|
||||
subtotal,
|
||||
shipping,
|
||||
total: subtotal + (shipping.rates.length > 0 ? shipping.rates[0].rate : 0)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
|
@ -179,6 +194,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
`SELECT ci.id, ci.quantity, ci.added_at,
|
||||
p.id AS product_id, p.name, p.description, p.price, p.stock_quantity,
|
||||
p.category_id, pc.name AS category_name,
|
||||
p.weight_grams, p.length_cm, p.width_cm, p.height_cm,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
|
|
@ -195,7 +211,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
JOIN products p ON ci.product_id = p.id
|
||||
JOIN product_categories pc ON p.category_id = pc.id
|
||||
WHERE ci.cart_id = $1
|
||||
GROUP BY ci.id, ci.quantity, ci.added_at, p.id, p.name, p.description, p.price, p.stock_quantity, p.category_id, pc.name`,
|
||||
GROUP BY ci.id, ci.quantity, ci.added_at, p.id, p.name, p.description, p.price, p.stock_quantity, p.category_id, pc.name, p.weight_grams, p.length_cm, p.width_cm, p.height_cm`,
|
||||
[cartId]
|
||||
);
|
||||
|
||||
|
|
@ -213,17 +229,29 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
};
|
||||
});
|
||||
|
||||
// Calculate total
|
||||
const total = processedItems.reduce((sum, item) => {
|
||||
// Calculate subtotal
|
||||
const subtotal = processedItems.reduce((sum, item) => {
|
||||
return sum + (parseFloat(item.price) * item.quantity);
|
||||
}, 0);
|
||||
|
||||
// Initialize shipping
|
||||
const shipping = {
|
||||
rates: []
|
||||
};
|
||||
|
||||
// Calculate basic flat rate shipping
|
||||
if (config.shipping.enabled) {
|
||||
shipping.rates = await shippingService.getFlatRateShipping(subtotal);
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: cartId,
|
||||
userId,
|
||||
items: processedItems,
|
||||
itemCount: processedItems.length,
|
||||
total
|
||||
subtotal,
|
||||
shipping,
|
||||
total: subtotal + (shipping.rates.length > 0 ? shipping.rates[0].rate : 0)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
|
@ -299,6 +327,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
`SELECT ci.id, ci.quantity, ci.added_at,
|
||||
p.id AS product_id, p.name, p.description, p.price, p.stock_quantity,
|
||||
p.category_id, pc.name AS category_name,
|
||||
p.weight_grams, p.length_cm, p.width_cm, p.height_cm,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
|
|
@ -315,7 +344,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
JOIN products p ON ci.product_id = p.id
|
||||
JOIN product_categories pc ON p.category_id = pc.id
|
||||
WHERE ci.cart_id = $1
|
||||
GROUP BY ci.id, ci.quantity, ci.added_at, p.id, p.name, p.description, p.price, p.stock_quantity, p.category_id, pc.name`,
|
||||
GROUP BY ci.id, ci.quantity, ci.added_at, p.id, p.name, p.description, p.price, p.stock_quantity, p.category_id, pc.name, p.weight_grams, p.length_cm, p.width_cm, p.height_cm`,
|
||||
[cartId]
|
||||
);
|
||||
|
||||
|
|
@ -333,17 +362,29 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
};
|
||||
});
|
||||
|
||||
// Calculate total
|
||||
const total = processedItems.reduce((sum, item) => {
|
||||
// Calculate subtotal
|
||||
const subtotal = processedItems.reduce((sum, item) => {
|
||||
return sum + (parseFloat(item.price) * item.quantity);
|
||||
}, 0);
|
||||
|
||||
// Initialize shipping
|
||||
const shipping = {
|
||||
rates: []
|
||||
};
|
||||
|
||||
// Calculate basic flat rate shipping
|
||||
if (config.shipping.enabled) {
|
||||
shipping.rates = await shippingService.getFlatRateShipping(subtotal);
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: cartId,
|
||||
userId,
|
||||
items: processedItems,
|
||||
itemCount: processedItems.length,
|
||||
total
|
||||
subtotal,
|
||||
shipping,
|
||||
total: subtotal + (shipping.rates.length > 0 ? shipping.rates[0].rate : 0)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
|
@ -386,6 +427,10 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
userId,
|
||||
items: [],
|
||||
itemCount: 0,
|
||||
subtotal: 0,
|
||||
shipping: {
|
||||
rates: []
|
||||
},
|
||||
total: 0
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -393,10 +438,104 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
}
|
||||
});
|
||||
|
||||
router.post('/checkout', async (req, res, next) => {
|
||||
// Get shipping rates for current cart
|
||||
router.post('/shipping-rates', async (req, res, next) => {
|
||||
try {
|
||||
const { userId, shippingAddress } = req.body;
|
||||
|
||||
if (req.user.id !== userId) {
|
||||
return res.status(403).json({
|
||||
error: true,
|
||||
message: 'You can only get shipping rates for your own cart'
|
||||
});
|
||||
}
|
||||
|
||||
// Shipping must be enabled
|
||||
if (!config.shipping.enabled) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
message: 'Shipping is currently disabled'
|
||||
});
|
||||
}
|
||||
|
||||
// Get cart
|
||||
const cartResult = await query(
|
||||
'SELECT * FROM carts WHERE user_id = $1',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (cartResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
error: true,
|
||||
message: 'Cart not found'
|
||||
});
|
||||
}
|
||||
|
||||
const cartId = cartResult.rows[0].id;
|
||||
|
||||
// Get cart items with product weights
|
||||
const cartItemsResult = await query(
|
||||
`SELECT ci.quantity, p.id, p.weight_grams, p.price
|
||||
FROM cart_items ci
|
||||
JOIN products p ON ci.product_id = p.id
|
||||
WHERE ci.cart_id = $1`,
|
||||
[cartId]
|
||||
);
|
||||
|
||||
if (cartItemsResult.rows.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
message: 'Cart is empty'
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate total weight and order value
|
||||
const totalWeight = shippingService.calculateTotalWeight(cartItemsResult.rows);
|
||||
const subtotal = cartItemsResult.rows.reduce((sum, item) => {
|
||||
return sum + (parseFloat(item.price) * item.quantity);
|
||||
}, 0);
|
||||
|
||||
// If no address provided, return only flat rate shipping
|
||||
if (!shippingAddress) {
|
||||
console.log("No Address provide flat rate");
|
||||
const rates = await shippingService.getFlatRateShipping(subtotal);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
rates
|
||||
});
|
||||
}
|
||||
|
||||
// Get real shipping rates
|
||||
const parsedAddress = typeof shippingAddress === 'string'
|
||||
? shippingService.parseAddressString(shippingAddress)
|
||||
: shippingAddress;
|
||||
console.log("parsedAddress provided ", parsedAddress);
|
||||
|
||||
const rates = await shippingService.getShippingRates(
|
||||
null, // Use default from config
|
||||
parsedAddress,
|
||||
{
|
||||
weight: totalWeight,
|
||||
order_total: subtotal
|
||||
}
|
||||
);
|
||||
console.log("rates provided ", JSON.stringify(rates, null ,4));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
rates
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting shipping rates:', error);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/checkout', async (req, res, next) => {
|
||||
try {
|
||||
const { userId, shippingAddress, shippingMethod } = req.body;
|
||||
|
||||
if (req.user.id !== userId) {
|
||||
return res.status(403).json({
|
||||
error: true,
|
||||
|
|
@ -421,7 +560,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
|
||||
// Get cart items
|
||||
const cartItemsResult = await query(
|
||||
`SELECT ci.*, p.price, p.name, p.description,
|
||||
`SELECT ci.*, p.price, p.name, p.description, p.weight_grams,
|
||||
(
|
||||
SELECT json_build_object(
|
||||
'path', pi.image_path,
|
||||
|
|
@ -444,11 +583,28 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
});
|
||||
}
|
||||
|
||||
// Calculate total
|
||||
const total = cartItemsResult.rows.reduce((sum, item) => {
|
||||
// Calculate subtotal
|
||||
const subtotal = cartItemsResult.rows.reduce((sum, item) => {
|
||||
return sum + (parseFloat(item.price) * item.quantity);
|
||||
}, 0);
|
||||
|
||||
// Determine shipping cost
|
||||
let shippingCost = 0;
|
||||
|
||||
if (config.shipping.enabled) {
|
||||
// If a specific shipping method was selected
|
||||
if (shippingMethod && shippingMethod.id) {
|
||||
shippingCost = parseFloat(shippingMethod.rate) || 0;
|
||||
} else {
|
||||
// Default to flat rate
|
||||
const shippingRates = await shippingService.getFlatRateShipping(subtotal);
|
||||
shippingCost = shippingRates[0].rate;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total with shipping
|
||||
const total = subtotal + shippingCost;
|
||||
|
||||
// Begin transaction
|
||||
const client = await pool.connect();
|
||||
|
||||
|
|
@ -458,8 +614,8 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
// Create order
|
||||
const orderId = uuidv4();
|
||||
await client.query(
|
||||
'INSERT INTO orders (id, user_id, status, total_amount, shipping_address, payment_completed) VALUES ($1, $2, $3, $4, $5, $6)',
|
||||
[orderId, userId, 'pending', total, shippingAddress, false]
|
||||
'INSERT INTO orders (id, user_id, status, total_amount, shipping_address, payment_completed, shipping_cost) VALUES ($1, $2, $3, $4, $5, $6, $7)',
|
||||
[orderId, userId, 'pending', total, shippingAddress, false, shippingCost]
|
||||
);
|
||||
|
||||
// Create order items
|
||||
|
|
@ -470,6 +626,22 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
);
|
||||
}
|
||||
|
||||
// If a shipping method was selected, save it with the order
|
||||
if (shippingMethod && shippingMethod.id) {
|
||||
const shippingInfo = {
|
||||
method_id: shippingMethod.id,
|
||||
carrier: shippingMethod.carrier,
|
||||
service: shippingMethod.service,
|
||||
rate: shippingMethod.rate,
|
||||
estimated_days: shippingMethod.delivery_days
|
||||
};
|
||||
|
||||
await client.query(
|
||||
'UPDATE orders SET shipping_info = $1 WHERE id = $2',
|
||||
[JSON.stringify(shippingInfo), orderId]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
// Send back cart items for Stripe checkout
|
||||
|
|
@ -478,6 +650,8 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
message: 'Order created successfully, ready for payment',
|
||||
orderId,
|
||||
cartItems: cartItemsResult.rows,
|
||||
subtotal,
|
||||
shippingCost,
|
||||
total
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
try {
|
||||
// Get all settings from database
|
||||
const allSettings = await SystemSettings.getAllSettings(pool, query);
|
||||
|
||||
config.updateFromDatabase(allSettings)
|
||||
// Build environment variables string
|
||||
let envContent = '';
|
||||
|
||||
|
|
|
|||
179
backend/src/routes/shipping.js
Normal file
179
backend/src/routes/shipping.js
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const shippingService = require('../services/shippingService.js');
|
||||
const config = require('../config');
|
||||
|
||||
module.exports = (pool, query, authMiddleware) => {
|
||||
// Apply authentication middleware to all routes
|
||||
router.use(authMiddleware);
|
||||
|
||||
/**
|
||||
* Get shipping rates
|
||||
* POST /api/shipping/rates
|
||||
*
|
||||
* Request Body:
|
||||
* {
|
||||
* address: {
|
||||
* name: string,
|
||||
* street: string,
|
||||
* city: string,
|
||||
* state: string,
|
||||
* zip: string,
|
||||
* country: string,
|
||||
* email: string
|
||||
* },
|
||||
* parcel: {
|
||||
* length: number,
|
||||
* width: number,
|
||||
* height: number,
|
||||
* weight: number,
|
||||
* order_total: number
|
||||
* },
|
||||
* items: [{ id, quantity, weight_grams }] // Optional cart items for weight calculation
|
||||
* }
|
||||
*/
|
||||
router.post('/rates', async (req, res, next) => {
|
||||
try {
|
||||
const { address, parcel, items } = req.body;
|
||||
|
||||
// Shipping must be enabled
|
||||
if (!config.shipping.enabled) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
message: 'Shipping is currently disabled'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!address || !parcel) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
message: 'Address and parcel information are required'
|
||||
});
|
||||
}
|
||||
|
||||
// If address is a string, parse it
|
||||
const parsedAddress = typeof address === 'string'
|
||||
? shippingService.parseAddressString(address)
|
||||
: address;
|
||||
|
||||
// Calculate total weight if items are provided
|
||||
if (items && items.length > 0) {
|
||||
parcel.weight = shippingService.calculateTotalWeight(items);
|
||||
}
|
||||
|
||||
// Get shipping rates
|
||||
const rates = await shippingService.getShippingRates(
|
||||
null, // Use default from config
|
||||
parsedAddress,
|
||||
{
|
||||
...parcel,
|
||||
order_total: parcel.order_total || 0
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
rates
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting shipping rates:', error);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Validate shipping address
|
||||
* POST /api/shipping/validate-address
|
||||
*
|
||||
* Request Body:
|
||||
* {
|
||||
* address: {
|
||||
* name: string,
|
||||
* street: string,
|
||||
* city: string,
|
||||
* state: string,
|
||||
* zip: string,
|
||||
* country: string,
|
||||
* email: string
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
router.post('/validate-address', async (req, res, next) => {
|
||||
try {
|
||||
const { address } = req.body;
|
||||
|
||||
// Shipping must be enabled
|
||||
if (!config.shipping.enabled) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
message: 'Shipping is currently disabled'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!address) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
message: 'Address information is required'
|
||||
});
|
||||
}
|
||||
|
||||
// If EasyPost is not enabled, just perform basic validation
|
||||
if (!config.shipping.easypostEnabled || !config.shipping.easypostApiKey) {
|
||||
const isValid = validateAddressFormat(address);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
valid: isValid,
|
||||
original_address: address,
|
||||
verified_address: address
|
||||
});
|
||||
}
|
||||
|
||||
// If address is a string, parse it
|
||||
const parsedAddress = typeof address === 'string'
|
||||
? shippingService.parseAddressString(address)
|
||||
: address;
|
||||
|
||||
// TODO: Implement EasyPost address verification
|
||||
// This would require making a call to EasyPost's API
|
||||
// For now, we'll return the original address
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
valid: true,
|
||||
original_address: parsedAddress,
|
||||
verified_address: parsedAddress
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error validating address:', error);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
return router;
|
||||
};
|
||||
|
||||
/**
|
||||
* Basic address format validation
|
||||
* @param {Object} address - Address to validate
|
||||
* @returns {boolean} Whether the address has valid format
|
||||
*/
|
||||
function validateAddressFormat(address) {
|
||||
// Check required fields
|
||||
if (!address.street || !address.city || !address.zip || !address.country) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check zip code format - just basic validation
|
||||
if (address.country === 'US' && !/^\d{5}(-\d{4})?$/.test(address.zip)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (address.country === 'CA' && !/^[A-Za-z]\d[A-Za-z] \d[A-Za-z]\d$/.test(address.zip)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
249
backend/src/services/shippingService.js
Normal file
249
backend/src/services/shippingService.js
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
|
||||
/**
|
||||
* Service for handling shipping operations with EasyPost API
|
||||
*/
|
||||
const shippingService = {
|
||||
/**
|
||||
* Create a shipment in EasyPost and get available rates
|
||||
* @param {Object} addressFrom - Shipping origin address
|
||||
* @param {Object} addressTo - Customer shipping address
|
||||
* @param {Object} parcelDetails - Package dimensions and weight
|
||||
* @returns {Promise<Array>} Array of available shipping rates
|
||||
*/
|
||||
async getShippingRates(addressFrom, addressTo, parcelDetails) {
|
||||
// If EasyPost is not enabled, return flat rate shipping
|
||||
if (!config.shipping.easypostEnabled || !config.shipping.easypostApiKey) {
|
||||
console.log("EASY POST NOT CONFIGURED ", !config.shipping.easypostEnabled, !config.shipping.easypostApiKey)
|
||||
return this.getFlatRateShipping(parcelDetails.order_total);
|
||||
}
|
||||
|
||||
try {
|
||||
// Format addresses for EasyPost
|
||||
const fromAddress = this.formatAddress(addressFrom || config.shipping.originAddress);
|
||||
const toAddress = this.formatAddress(addressTo);
|
||||
|
||||
// Format parcel for EasyPost
|
||||
const parcel = this.formatParcel(parcelDetails);
|
||||
console.log("EasyPost shipment request", JSON.stringify({
|
||||
shipment: {
|
||||
from_address: fromAddress,
|
||||
to_address: toAddress,
|
||||
parcel: parcel
|
||||
}
|
||||
}, null , 4))
|
||||
// Create shipment via EasyPost API
|
||||
const response = await axios.post(
|
||||
'https://api.easypost.com/v2/shipments',
|
||||
{
|
||||
shipment: {
|
||||
from_address: fromAddress,
|
||||
to_address: toAddress,
|
||||
parcel: parcel
|
||||
}
|
||||
},
|
||||
{
|
||||
auth: {
|
||||
username: config.shipping.easypostApiKey,
|
||||
password: ''
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
// console.log("EasyPost shipment response", response)
|
||||
|
||||
// Process and filter rates
|
||||
return this.processShippingRates(response.data.rates, parcelDetails.order_total);
|
||||
} catch (error) {
|
||||
console.error('EasyPost API error:', error.response?.data || error.message);
|
||||
// Fallback to flat rate if API fails
|
||||
return this.getFlatRateShipping(parcelDetails.order_total);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Format address for EasyPost API
|
||||
* @param {Object} address - Address details
|
||||
* @returns {Object} Formatted address object
|
||||
*/
|
||||
formatAddress(address) {
|
||||
return {
|
||||
street1: address.street || address.street1,
|
||||
city: address.city,
|
||||
state: address.state || address.province,
|
||||
zip: address.zip || address.postalCode,
|
||||
country: address.country,
|
||||
name: address.name || undefined,
|
||||
company: address.company || undefined,
|
||||
phone: address.phone || undefined,
|
||||
email: address.email || undefined
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Format parcel for EasyPost API
|
||||
* @param {Object} parcelDetails - Package dimensions and weight
|
||||
* @returns {Object} Formatted parcel object
|
||||
*/
|
||||
formatParcel(parcelDetails) {
|
||||
const pkg = config.shipping.defaultPackage;
|
||||
|
||||
// Convert weight to ounces if coming from grams
|
||||
const weight = parcelDetails.weight || 500; // Default to 500g if not provided
|
||||
const weightOz = pkg.weightUnit === 'g' ? weight * 0.035274 : weight;
|
||||
|
||||
// Convert dimensions to inches if coming from cm
|
||||
const lengthConversionFactor = pkg.unit === 'cm' ? 0.393701 : 1;
|
||||
|
||||
return {
|
||||
length: (parcelDetails.length || pkg.length) * lengthConversionFactor,
|
||||
width: (parcelDetails.width || pkg.width) * lengthConversionFactor,
|
||||
height: (parcelDetails.height || pkg.height) * lengthConversionFactor,
|
||||
weight: weightOz,
|
||||
predefined_package: parcelDetails.predefined_package || null
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Process and filter shipping rates from EasyPost
|
||||
* @param {Array} rates - EasyPost shipping rates
|
||||
* @param {number} orderTotal - Order total amount
|
||||
* @returns {Array} Processed shipping rates
|
||||
*/
|
||||
processShippingRates(rates, orderTotal) {
|
||||
if (!rates || !Array.isArray(rates)) {
|
||||
return this.getFlatRateShipping(orderTotal);
|
||||
}
|
||||
|
||||
// Filter by allowed carriers
|
||||
let filteredRates = rates.filter(rate =>
|
||||
config.shipping.carriersAllowed.some(carrier =>
|
||||
rate.carrier.toUpperCase().includes(carrier.toUpperCase())
|
||||
)
|
||||
);
|
||||
|
||||
if (filteredRates.length === 0) {
|
||||
return this.getFlatRateShipping(orderTotal);
|
||||
}
|
||||
|
||||
// Format rates to standardized format
|
||||
const formattedRates = filteredRates.map(rate => ({
|
||||
id: rate.id,
|
||||
carrier: rate.carrier,
|
||||
service: rate.service,
|
||||
rate: parseFloat(rate.rate),
|
||||
currency: rate.currency,
|
||||
delivery_days: rate.delivery_days || 'Unknown',
|
||||
delivery_date: rate.delivery_date || null,
|
||||
delivery_time: rate.est_delivery_time || null
|
||||
}));
|
||||
|
||||
// Check if free shipping applies
|
||||
if (orderTotal >= config.shipping.freeThreshold) {
|
||||
formattedRates.push({
|
||||
id: 'free-shipping',
|
||||
carrier: 'FREE',
|
||||
service: 'Standard Shipping',
|
||||
rate: 0,
|
||||
currency: 'USD',
|
||||
delivery_days: '5-7',
|
||||
delivery_date: null,
|
||||
delivery_time: null
|
||||
});
|
||||
}
|
||||
|
||||
return formattedRates;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get flat rate shipping as fallback
|
||||
* @param {number} orderTotal - Order total amount
|
||||
* @returns {Array} Flat rate shipping options
|
||||
*/
|
||||
getFlatRateShipping(orderTotal) {
|
||||
const shippingOptions = [{
|
||||
id: 'flat-rate',
|
||||
carrier: 'Standard',
|
||||
service: 'Flat Rate Shipping',
|
||||
rate: config.shipping.flatRate,
|
||||
currency: 'USD',
|
||||
delivery_days: '5-7',
|
||||
delivery_date: null,
|
||||
delivery_time: null
|
||||
}];
|
||||
|
||||
// Add free shipping if order qualifies
|
||||
if (orderTotal >= config.shipping.freeThreshold) {
|
||||
shippingOptions.push({
|
||||
id: 'free-shipping',
|
||||
carrier: 'FREE',
|
||||
service: 'Standard Shipping',
|
||||
rate: 0,
|
||||
currency: 'USD',
|
||||
delivery_days: '5-7',
|
||||
delivery_date: null,
|
||||
delivery_time: null
|
||||
});
|
||||
}
|
||||
|
||||
return shippingOptions;
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse shipping address from string format
|
||||
* @param {string} addressString - Shipping address as string
|
||||
* @returns {Object} Parsed address object
|
||||
*/
|
||||
parseAddressString(addressString) {
|
||||
const lines = addressString.trim().split('\n').map(line => line.trim());
|
||||
|
||||
// Try to intelligently parse the address components
|
||||
// This is a simplified version - might need enhancement for edge cases
|
||||
const parsedAddress = {
|
||||
name: lines[0] || '',
|
||||
street: lines[1] || '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
country: lines[lines.length - 1] || ''
|
||||
};
|
||||
|
||||
// Try to parse city, state, zip from line 2
|
||||
if (lines[2]) {
|
||||
const cityStateZip = lines[2].split(',');
|
||||
if (cityStateZip.length >= 2) {
|
||||
parsedAddress.city = cityStateZip[0].trim();
|
||||
|
||||
// Split state and zip
|
||||
const stateZip = cityStateZip[1].trim().split(' ');
|
||||
if (stateZip.length >= 2) {
|
||||
parsedAddress.state = stateZip[0].trim();
|
||||
parsedAddress.zip = stateZip.slice(1).join(' ').trim();
|
||||
} else {
|
||||
parsedAddress.state = stateZip[0].trim();
|
||||
}
|
||||
} else {
|
||||
parsedAddress.city = lines[2];
|
||||
}
|
||||
}
|
||||
|
||||
return parsedAddress;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate total shipping weight from cart items
|
||||
* @param {Array} items - Cart items
|
||||
* @returns {number} Total weight in grams
|
||||
*/
|
||||
calculateTotalWeight(items) {
|
||||
return items.reduce((total, item) => {
|
||||
const itemWeight = item.weight_grams || 100;
|
||||
return total + (itemWeight * item.quantity);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = shippingService;
|
||||
|
|
@ -40,5 +40,18 @@ VALUES
|
|||
-- Shipping Settings
|
||||
('shipping_flat_rate', '10.00', 'shipping'),
|
||||
('shipping_free_threshold', '50.00', 'shipping'),
|
||||
('shipping_enabled', 'true', 'shipping')
|
||||
('shipping_enabled', 'true', 'shipping'),
|
||||
('easypost_api_key', NULL, 'shipping'),
|
||||
('easypost_enabled', 'false', 'shipping'),
|
||||
('shipping_origin_street', '123 Main St', 'shipping'),
|
||||
('shipping_origin_city', 'Vancouver', 'shipping'),
|
||||
('shipping_origin_state', 'BC', 'shipping'),
|
||||
('shipping_origin_zip', 'V6K 1V6', 'shipping'),
|
||||
('shipping_origin_country', 'CA', 'shipping'),
|
||||
('shipping_default_package_length', '15', 'shipping'),
|
||||
('shipping_default_package_width', '12', 'shipping'),
|
||||
('shipping_default_package_height', '10', 'shipping'),
|
||||
('shipping_default_package_unit', 'cm', 'shipping'),
|
||||
('shipping_default_weight_unit', 'g', 'shipping'),
|
||||
('shipping_carriers_allowed', 'USPS,UPS,FedEx,DHL,Canada Post,Purolator', 'shipping')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
10
db/init/12-shipping-orders.sql
Normal file
10
db/init/12-shipping-orders.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- Add shipping cost column to orders table
|
||||
ALTER TABLE orders ADD COLUMN IF NOT EXISTS shipping_cost DECIMAL(10, 2) DEFAULT 0.00;
|
||||
|
||||
-- Update shipping info to be JSONB if not already
|
||||
ALTER TABLE orders ALTER COLUMN shipping_info TYPE JSONB
|
||||
USING CASE
|
||||
WHEN shipping_info IS NULL THEN NULL
|
||||
WHEN jsonb_typeof(shipping_info::jsonb) = 'object' THEN shipping_info::jsonb
|
||||
ELSE jsonb_build_object('data', shipping_info)
|
||||
END;
|
||||
|
|
@ -1,117 +1,136 @@
|
|||
Rocks/
|
||||
├── .git/ # Git repository
|
||||
├── .env # Environment configuration
|
||||
├── .gitignore # Git ignore file
|
||||
├── README.md # Project documentation
|
||||
├── Dockerfile # Main Dockerfile
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
├── nginx.conf # Nginx configuration
|
||||
├── setup-frontend.sh # Frontend setup script
|
||||
├── package.json # Project dependencies
|
||||
├── index.html # Main HTML entry point
|
||||
├── frontend/
|
||||
│ ├── node_modules/ # Node.js dependencies
|
||||
│ ├── public/ # Static public assets
|
||||
│ └── src/
|
||||
│ ├── assets/ # Static assets
|
||||
│ ├── components/
|
||||
│ │ ├── EmailDialog.jsx # Email dialog component
|
||||
│ │ ├── Footer.jsx # Footer component
|
||||
│ │ ├── ImageUploader.jsx # Image upload component
|
||||
│ │ ├── Notifications.jsx # Notifications component
|
||||
│ │ ├── ProductImage.jsx # Product image component
|
||||
│ │ └── ProtectedRoute.jsx # Auth route protection
|
||||
│ ├── features/
|
||||
│ │ ├── ui/
|
||||
│ │ │ └── uiSlice.js # UI state management
|
||||
│ │ ├── cart/
|
||||
│ │ │ └── cartSlice.js # Cart state management
|
||||
│ │ ├── auth/
|
||||
│ │ │ └── authSlice.js # Auth state management
|
||||
│ │ └── store/
|
||||
│ │ └── index.js # Redux store configuration
|
||||
│ ├── hooks/
|
||||
│ │ ├── reduxHooks.js # Redux related hooks
|
||||
│ │ ├── apiHooks.js # API related hooks
|
||||
│ │ └── settingsAdminHooks.js # Admin settings hooks
|
||||
│ ├── layouts/
|
||||
│ │ ├── AdminLayout.jsx # Admin area layout
|
||||
│ │ ├── MainLayout.jsx # Main site layout
|
||||
│ │ └── AuthLayout.jsx # Authentication layout
|
||||
│ ├── pages/
|
||||
│ │ ├── Admin/
|
||||
│ │ │ ├── DashboardPage.jsx # Admin dashboard
|
||||
│ │ │ ├── ProductsPage.jsx # Products management
|
||||
│ │ │ ├── ProductEditPage.jsx # Product editing
|
||||
│ │ │ ├── OrdersPage.jsx # Orders management
|
||||
│ │ │ ├── CategoriesPage.jsx # Categories management
|
||||
│ │ │ ├── CustomersPage.jsx # Customer management
|
||||
│ │ │ └── SettingsPage.jsx # Site settings
|
||||
│ │ ├── HomePage.jsx # Home page
|
||||
│ │ ├── ProductsPage.jsx # Products listing
|
||||
│ │ ├── ProductDetailPage.jsx # Product details
|
||||
│ │ ├── CartPage.jsx # Shopping cart
|
||||
│ │ ├── CheckoutPage.jsx # Checkout process
|
||||
│ │ ├── LoginPage.jsx # Login page
|
||||
│ │ ├── RegisterPage.jsx # Registration page
|
||||
│ │ ├── VerifyPage.jsx # Email verification
|
||||
│ │ └── NotFoundPage.jsx # 404 page
|
||||
│ ├── services/
|
||||
│ │ ├── api.js # API client
|
||||
│ │ ├── authService.js # Authentication service
|
||||
│ │ ├── cartService.js # Cart management service
|
||||
│ │ ├── productService.js # Products service
|
||||
│ │ ├── settingsAdminService.js # Settings service
|
||||
│ │ ├── adminService.js # Admin service
|
||||
│ │ ├── categoryAdminService.js # Category service
|
||||
│ │ └── imageService.js # Image handling service
|
||||
│ ├── theme/
|
||||
│ │ ├── index.js # Theme configuration
|
||||
│ │ └── ThemeProvider.jsx # Theme provider component
|
||||
│ ├── utils/
|
||||
│ │ └── imageUtils.js # Image handling utilities
|
||||
│ ├── App.jsx # Main application component
|
||||
│ ├── main.jsx # Application entry point
|
||||
│ ├── config.js # Frontend configuration
|
||||
│ └── vite.config.js # Vite bundler configuration
|
||||
│ ├── node_modules/
|
||||
│ ├── src/
|
||||
│ │ ├── pages/
|
||||
│ │ │ ├── Admin/
|
||||
│ │ │ │ ├── OrdersPage.jsx
|
||||
│ │ │ │ ├── SettingsPage.jsx
|
||||
│ │ │ │ ├── CustomersPage.jsx
|
||||
│ │ │ │ ├── ProductEditPage.jsx
|
||||
│ │ │ │ ├── DashboardPage.jsx
|
||||
│ │ │ │ ├── CategoriesPage.jsx
|
||||
│ │ │ │ └── ProductsPage.jsx
|
||||
│ │ │ ├── PaymentSuccessPage.jsx
|
||||
│ │ │ ├── CheckoutPage.jsx
|
||||
│ │ │ ├── UserOrdersPage.jsx
|
||||
│ │ │ ├── PaymentCancelPage.jsx
|
||||
│ │ │ ├── ProductDetailPage.jsx
|
||||
│ │ │ ├── CartPage.jsx
|
||||
│ │ │ ├── ProductsPage.jsx
|
||||
│ │ │ ├── HomePage.jsx
|
||||
│ │ │ ├── VerifyPage.jsx
|
||||
│ │ │ ├── RegisterPage.jsx
|
||||
│ │ │ ├── NotFoundPage.jsx
|
||||
│ │ │ └── LoginPage.jsx
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── OrderStatusDialog.jsx
|
||||
│ │ │ ├── StripePaymentForm.jsx
|
||||
│ │ │ ├── EmailDialog.jsx
|
||||
│ │ │ ├── Footer.jsx
|
||||
│ │ │ ├── ImageUploader.jsx
|
||||
│ │ │ ├── ProductImage.jsx
|
||||
│ │ │ ├── ProtectedRoute.jsx
|
||||
│ │ │ └── Notifications.jsx
|
||||
│ │ ├── context/
|
||||
│ │ │ └── StripeContext.jsx
|
||||
│ │ ├── hooks/
|
||||
│ │ │ ├── apiHooks.js
|
||||
│ │ │ ├── adminHooks.js
|
||||
│ │ │ ├── reduxHooks.js
|
||||
│ │ │ ├── settingsAdminHooks.js
|
||||
│ │ │ └── categoryAdminHooks.js
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── adminService.js
|
||||
│ │ │ ├── authService.js
|
||||
│ │ │ ├── settingsAdminService.js
|
||||
│ │ │ ├── cartService.js
|
||||
│ │ │ ├── categoryAdminService.js
|
||||
│ │ │ ├── imageService.js
|
||||
│ │ │ ├── productService.js
|
||||
│ │ │ └── api.js
|
||||
│ │ ├── utils/
|
||||
│ │ │ └── imageUtils.js
|
||||
│ │ ├── layouts/
|
||||
│ │ │ ├── MainLayout.jsx
|
||||
│ │ │ ├── AdminLayout.jsx
|
||||
│ │ │ └── AuthLayout.jsx
|
||||
│ │ ├── theme/
|
||||
│ │ │ ├── index.js
|
||||
│ │ │ └── ThemeProvider.jsx
|
||||
│ │ ├── features/
|
||||
│ │ │ ├── ui/
|
||||
│ │ │ │ └── uiSlice.js
|
||||
│ │ │ ├── cart/
|
||||
│ │ │ │ └── cartSlice.js
|
||||
│ │ │ ├── auth/
|
||||
│ │ │ │ └── authSlice.js
|
||||
│ │ │ └── store/
|
||||
│ │ │ └── index.js
|
||||
│ │ ├── assets/
|
||||
│ │ ├── App.jsx
|
||||
│ │ ├── config.js
|
||||
│ │ └── main.jsx
|
||||
│ └── public/
|
||||
│ ├── favicon.svg
|
||||
│ ├── package-lock.json
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.js
|
||||
│ ├── Dockerfile
|
||||
│ ├── nginx.conf
|
||||
│ ├── index.html
|
||||
│ ├── README.md
|
||||
│ ├── .env
|
||||
│ └── setup-frontend.sh
|
||||
├── backend/
|
||||
│ ├── node_modules/ # Node.js dependencies
|
||||
│ ├── public/
|
||||
│ │ └── uploads/
|
||||
│ │ └── products/ # Product images storage
|
||||
│ ├── src/
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── auth.js # Authentication routes
|
||||
│ │ │ ├── userAdmin.js # User administration
|
||||
│ │ │ ├── products.js # Product routes
|
||||
│ │ │ ├── productAdmin.js # Product administration
|
||||
│ │ │ ├── cart.js # Shopping cart routes
|
||||
│ │ │ ├── settingsAdmin.js # Settings administration
|
||||
│ │ │ ├── images.js # Image handling routes
|
||||
│ │ │ ├── categoryAdmin.js # Category administration
|
||||
│ │ │ └── orderAdmin.js # Order administration
|
||||
│ │ ├── middleware/
|
||||
│ │ │ ├── auth.js # Authentication middleware
|
||||
│ │ │ ├── adminAuth.js # Admin authentication
|
||||
│ │ │ └── upload.js # File upload middleware
|
||||
│ │ │ ├── userOrders.js
|
||||
│ │ │ ├── orderAdmin.js
|
||||
│ │ │ ├── stripePayment.js
|
||||
│ │ │ ├── cart.js
|
||||
│ │ │ ├── auth.js
|
||||
│ │ │ ├── userAdmin.js
|
||||
│ │ │ ├── settingsAdmin.js
|
||||
│ │ │ ├── products.js
|
||||
│ │ │ ├── categoryAdmin.js
|
||||
│ │ │ ├── productAdminImages.js
|
||||
│ │ │ ├── images.js
|
||||
│ │ │ └── productAdmin.js
|
||||
│ │ ├── models/
|
||||
│ │ │ └── SystemSettings.js # System settings model
|
||||
│ │ │ └── SystemSettings.js
|
||||
│ │ ├── middleware/
|
||||
│ │ │ ├── upload.js
|
||||
│ │ │ ├── auth.js
|
||||
│ │ │ └── adminAuth.js
|
||||
│ │ ├── db/
|
||||
│ │ │ └── index.js # Database setup
|
||||
│ │ ├── config.js # Backend configuration
|
||||
│ │ └── index.js # Server entry point
|
||||
│ ├── .env # Backend environment variables
|
||||
│ ├── Dockerfile # Backend Dockerfile
|
||||
│ └── package.json # Backend dependencies
|
||||
└── db/
|
||||
├── init/
|
||||
│ ├── 01-schema.sql # Main database schema
|
||||
│ ├── 02-seed.sql # Initial seed data
|
||||
│ ├── 03-api-key.sql # API key setup
|
||||
│ ├── 04-product-images.sql # Product images schema
|
||||
│ ├── 05-admin-role.sql # Admin role definition
|
||||
│ ├── 06-product-categories.sql # Product categories
|
||||
│ ├── 07-user-keys.sql # User API keys
|
||||
│ ├── 08-create-email.sql # Email templates
|
||||
│ └── 09-system-settings.sql # System settings
|
||||
└── test/ # Test database scripts
|
||||
│ │ │ └── index.js
|
||||
│ │ ├── index.js
|
||||
│ │ └── config.js
|
||||
│ ├── public/
|
||||
│ │ └── uploads/
|
||||
│ │ └── products/
|
||||
│ ├── node_modules/
|
||||
│ ├── .env
|
||||
│ ├── package.json
|
||||
│ ├── Dockerfile
|
||||
│ ├── README.md
|
||||
│ └── .gitignore
|
||||
├── db/
|
||||
│ ├── init/
|
||||
│ │ ├── 01-schema.sql
|
||||
│ │ ├── 02-seed.sql
|
||||
│ │ ├── 03-api-key.sql
|
||||
│ │ ├── 04-product-images.sql
|
||||
│ │ ├── 05-admin-role.sql
|
||||
│ │ ├── 06-product-categories.sql
|
||||
│ │ ├── 07-user-keys.sql
|
||||
│ │ ├── 08-create-email.sql
|
||||
│ │ ├── 09-system-settings.sql
|
||||
│ │ ├── 10-payment.sql
|
||||
│ │ └── 11-notifications.sql
|
||||
│ └── .gitignore
|
||||
├── test/
|
||||
├── fileStructure.txt
|
||||
├── docker-compose.yml
|
||||
└── .gitignore
|
||||
|
|
@ -16,16 +16,21 @@ import {
|
|||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Alert
|
||||
Alert,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
FormControl,
|
||||
FormLabel
|
||||
} from '@mui/material';
|
||||
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||||
import { useAuth, useCart } from '../hooks/reduxHooks';
|
||||
import { useCheckout } from '../hooks/apiHooks';
|
||||
import { useStripe, StripeElementsProvider } from '../context/StripeContext';
|
||||
import StripePaymentForm from '../components/StripePaymentForm';
|
||||
import apiClient from '../services/api';
|
||||
|
||||
// Checkout steps
|
||||
const steps = ['Shipping Address', 'Review Order', 'Payment', 'Confirmation'];
|
||||
const steps = ['Shipping Address', 'Shipping Method', 'Review Order', 'Payment', 'Confirmation'];
|
||||
|
||||
const CheckoutPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -37,10 +42,16 @@ const CheckoutPage = () => {
|
|||
// State for checkout steps
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [isLoadingShipping, setIsLoadingShipping] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [orderId, setOrderId] = useState(null);
|
||||
const [checkoutUrl, setCheckoutUrl] = useState(null);
|
||||
|
||||
// State for shipping options
|
||||
const [shippingRates, setShippingRates] = useState([]);
|
||||
const [selectedShippingMethod, setSelectedShippingMethod] = useState(null);
|
||||
const [shippingCost, setShippingCost] = useState(0);
|
||||
|
||||
// State for form data
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: userData?.first_name || '',
|
||||
|
|
@ -75,10 +86,22 @@ const CheckoutPage = () => {
|
|||
if (!validateShippingForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If valid, fetch shipping rates
|
||||
fetchShippingRates();
|
||||
}
|
||||
|
||||
// If on shipping method step, validate selection
|
||||
if (activeStep === 1) {
|
||||
if (!selectedShippingMethod) {
|
||||
setError('Please select a shipping method');
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
}
|
||||
|
||||
// If on review step, process checkout
|
||||
if (activeStep === 1) {
|
||||
if (activeStep === 2) {
|
||||
handlePlaceOrder();
|
||||
return;
|
||||
}
|
||||
|
|
@ -98,7 +121,7 @@ const CheckoutPage = () => {
|
|||
for (const field of requiredFields) {
|
||||
if (!formData[field]) {
|
||||
// In a real app, you'd set specific errors for each field
|
||||
alert(`Please fill in all required fields`);
|
||||
setError(`Please fill in all required fields`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -106,13 +129,70 @@ const CheckoutPage = () => {
|
|||
// Basic email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(formData.email)) {
|
||||
alert('Please enter a valid email address');
|
||||
setError('Please enter a valid email address');
|
||||
return false;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Fetch shipping rates based on address
|
||||
const fetchShippingRates = async () => {
|
||||
try {
|
||||
setIsLoadingShipping(true);
|
||||
setError(null);
|
||||
|
||||
// Format shipping address
|
||||
const shippingAddress = {
|
||||
name: `${formData.firstName} ${formData.lastName}`,
|
||||
street: formData.address,
|
||||
city: formData.city,
|
||||
state: formData.province,
|
||||
zip: formData.postalCode,
|
||||
country: formData.country,
|
||||
email: formData.email
|
||||
};
|
||||
|
||||
// Call API to get shipping rates
|
||||
const response = await apiClient.post('/cart/shipping-rates', {
|
||||
userId: user,
|
||||
shippingAddress
|
||||
});
|
||||
|
||||
if (response.data.rates && response.data.rates.length > 0) {
|
||||
setShippingRates(response.data.rates);
|
||||
// Default to lowest cost option
|
||||
const lowestCostOption = response.data.rates.reduce(
|
||||
(lowest, current) => current.rate < lowest.rate ? current : lowest,
|
||||
response.data.rates[0]
|
||||
);
|
||||
setSelectedShippingMethod(lowestCostOption);
|
||||
setShippingCost(lowestCostOption.rate);
|
||||
} else {
|
||||
setShippingRates([]);
|
||||
setSelectedShippingMethod(null);
|
||||
setShippingCost(0);
|
||||
setError('No shipping options available for this address');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching shipping rates:', error);
|
||||
setError('Failed to retrieve shipping options. Please try again.');
|
||||
} finally {
|
||||
setIsLoadingShipping(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle shipping method selection
|
||||
const handleShippingMethodChange = (event) => {
|
||||
const selectedMethodId = event.target.value;
|
||||
const method = shippingRates.find(rate => rate.id === selectedMethodId);
|
||||
if (method) {
|
||||
setSelectedShippingMethod(method);
|
||||
setShippingCost(method.rate);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle place order
|
||||
const handlePlaceOrder = async () => {
|
||||
if (!user || !items || items.length === 0) {
|
||||
|
|
@ -133,21 +213,26 @@ const CheckoutPage = () => {
|
|||
// Call the checkout API to create the order
|
||||
const orderResponse = await checkout.mutateAsync({
|
||||
userId: user,
|
||||
shippingAddress
|
||||
shippingAddress,
|
||||
shippingMethod: selectedShippingMethod
|
||||
});
|
||||
|
||||
// Store the order ID for later use
|
||||
setOrderId(orderResponse.orderId);
|
||||
|
||||
// Proceed to payment step
|
||||
setActiveStep(2);
|
||||
setActiveStep(3);
|
||||
|
||||
// Create a Stripe checkout session
|
||||
const session = await createCheckoutSession(
|
||||
orderResponse.cartItems,
|
||||
orderResponse.orderId,
|
||||
shippingAddress,
|
||||
user
|
||||
user,
|
||||
{
|
||||
shipping_cost: orderResponse.shippingCost || shippingCost,
|
||||
shipping_method: selectedShippingMethod ? selectedShippingMethod.carrier + ' - ' + selectedShippingMethod.service : 'Standard Shipping'
|
||||
}
|
||||
);
|
||||
|
||||
// Redirect to Stripe Checkout
|
||||
|
|
@ -311,6 +396,65 @@ const CheckoutPage = () => {
|
|||
</Box>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Shipping Method
|
||||
</Typography>
|
||||
|
||||
{isLoadingShipping ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : shippingRates.length > 0 ? (
|
||||
<FormControl component="fieldset" sx={{ width: '100%' }}>
|
||||
<RadioGroup
|
||||
aria-label="shipping-method"
|
||||
name="shipping-method"
|
||||
value={selectedShippingMethod?.id || ''}
|
||||
onChange={handleShippingMethodChange}
|
||||
>
|
||||
{shippingRates.map((rate) => (
|
||||
<Paper
|
||||
key={rate.id}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
mb: 2,
|
||||
p: 2,
|
||||
border: selectedShippingMethod?.id === rate.id ? 2 : 1,
|
||||
borderColor: selectedShippingMethod?.id === rate.id ? 'primary.main' : 'divider'
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={rate.id}
|
||||
control={<Radio />}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="subtitle1">
|
||||
{rate.carrier} - {rate.service}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Estimated delivery: {rate.delivery_days} days
|
||||
</Typography>
|
||||
<Typography variant="h6" color="primary" sx={{ mt: 1 }}>
|
||||
{rate.rate > 0 ? `$${rate.rate.toFixed(2)}` : 'FREE'}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
sx={{ width: '100%', m: 0 }}
|
||||
/>
|
||||
</Paper>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
) : (
|
||||
<Alert severity="warning">
|
||||
No shipping options available for this address. Please check your shipping address or contact support.
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
{/* Order summary */}
|
||||
|
|
@ -332,14 +476,24 @@ const CheckoutPage = () => {
|
|||
))}
|
||||
|
||||
<ListItem sx={{ py: 1, px: 0 }}>
|
||||
<ListItemText primary="Shipping" />
|
||||
<Typography variant="body2">Free</Typography>
|
||||
<ListItemText primary="Subtotal" />
|
||||
<Typography variant="body2">${total.toFixed(2)}</Typography>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ py: 1, px: 0 }}>
|
||||
<ListItemText
|
||||
primary="Shipping"
|
||||
secondary={selectedShippingMethod ? `${selectedShippingMethod.carrier} - ${selectedShippingMethod.service}` : 'Standard Shipping'}
|
||||
/>
|
||||
<Typography variant="body2">
|
||||
{shippingCost > 0 ? `$${shippingCost.toFixed(2)}` : 'Free'}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ py: 1, px: 0 }}>
|
||||
<ListItemText primary="Total" />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
${total.toFixed(2)}
|
||||
${(total + shippingCost).toFixed(2)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
|
@ -368,7 +522,7 @@ const CheckoutPage = () => {
|
|||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
case 2:
|
||||
case 3:
|
||||
return (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
{isStripeLoading || isProcessing ? (
|
||||
|
|
@ -391,7 +545,7 @@ const CheckoutPage = () => {
|
|||
)}
|
||||
</Box>
|
||||
);
|
||||
case 3:
|
||||
case 4:
|
||||
return (
|
||||
<Box sx={{ mt: 3, textAlign: 'center' }}>
|
||||
<Alert severity="success" sx={{ mb: 3 }}>
|
||||
|
|
@ -440,31 +594,39 @@ const CheckoutPage = () => {
|
|||
</Stepper>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 3 }}>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{getStepContent(activeStep)}
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 3 }}>
|
||||
{activeStep !== 0 && activeStep !== 3 && !isProcessing && (
|
||||
{activeStep !== 0 && activeStep !== 3 && activeStep !== 4 && !isProcessing && !isLoadingShipping && (
|
||||
<Button
|
||||
onClick={handleBack}
|
||||
sx={{ mr: 1 }}
|
||||
disabled={checkout.isLoading || isProcessing}
|
||||
disabled={checkout.isLoading || isProcessing || isLoadingShipping}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{activeStep !== 2 && activeStep !== 3 ? (
|
||||
{activeStep !== 3 && activeStep !== 4 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleNext}
|
||||
disabled={checkout.isLoading || isProcessing}
|
||||
disabled={checkout.isLoading || isProcessing || isLoadingShipping}
|
||||
>
|
||||
{activeStep === steps.length - 2 ? 'Place Order' : 'Next'}
|
||||
{(checkout.isLoading || isProcessing) && (
|
||||
{activeStep === steps.length - 3 ? 'Place Order' : 'Next'}
|
||||
{(checkout.isLoading || isProcessing || isLoadingShipping) && (
|
||||
<CircularProgress size={24} sx={{ ml: 1 }} />
|
||||
)}
|
||||
</Button>
|
||||
) : activeStep === 3 ? (
|
||||
)}
|
||||
|
||||
{activeStep === 4 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
component={RouterLink}
|
||||
|
|
@ -472,7 +634,7 @@ const CheckoutPage = () => {
|
|||
>
|
||||
Return to Home
|
||||
</Button>
|
||||
) : null}
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
|
|
|||
Loading…
Reference in a new issue