From 9974d41bd86bf70fff65123f1b866e84e85282df Mon Sep 17 00:00:00 2001 From: ilya_zahvatkin Date: Sun, 27 Apr 2025 03:00:13 +0700 Subject: [PATCH] =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D1=8B!!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 10244 -> 10244 bytes .cursor/rules/fastapinextjs.mdc | 2 + .gitignore | 1 + backend/.DS_Store | Bin 6148 -> 6148 bytes backend/.env | 2 +- backend/.env.docker | 2 +- backend/Dockerfile | 10 + backend/alembic.ini | 2 +- .../versions/7192b0707277_add_new_order.py | 42 + ...7192b0707277_add_new_order.cpython-310.pyc | Bin 0 -> 1395 bytes ..._table_and_update_product_.cpython-310.pyc | Bin 3424 -> 2135 bytes ...81393a28fee_add_new_order_.cpython-310.pyc | Bin 0 -> 878 bytes ...89a59b0e814_add_new_order_.cpython-310.pyc | Bin 0 -> 1121 bytes .../versions/f89a59b0e814_add_new_order_.py | 30 + backend/app/.DS_Store | Bin 6148 -> 6148 bytes .../app/__pycache__/config.cpython-310.pyc | Bin 2962 -> 2902 bytes backend/app/config.py | 2 +- .../__pycache__/order_models.cpython-310.pyc | Bin 2887 -> 3081 bytes backend/app/models/order_models.py | 25 +- .../__pycache__/catalog_repo.cpython-310.pyc | Bin 17399 -> 18214 bytes .../__pycache__/content_repo.cpython-310.pyc | Bin 6106 -> 6112 bytes .../__pycache__/order_repo.cpython-310.pyc | Bin 11947 -> 15351 bytes .../__pycache__/user_repo.cpython-310.pyc | Bin 6498 -> 7958 bytes backend/app/repositories/catalog_repo.py | 24 +- backend/app/repositories/content_repo.py | 80 +- backend/app/repositories/order_repo.py | 351 +- backend/app/repositories/user_repo.py | 105 +- .../catalog_router.cpython-310.pyc | Bin 15906 -> 16743 bytes .../__pycache__/order_router.cpython-310.pyc | Bin 4232 -> 5378 bytes backend/app/routers/catalog_router.py | 53 +- backend/app/routers/order_router.py | 78 +- .../__pycache__/order_schemas.cpython-310.pyc | Bin 4924 -> 9054 bytes backend/app/schemas/order_schemas.py | 148 +- backend/app/services/__init__.py | 2 +- .../__pycache__/__init__.cpython-310.pyc | Bin 1989 -> 2016 bytes .../catalog_service.cpython-310.pyc | Bin 24741 -> 25312 bytes .../__pycache__/order_service.cpython-310.pyc | Bin 5905 -> 7774 bytes backend/app/services/catalog_service.py | 28 +- backend/app/services/order_service.py | 133 +- backend/docs/api_documentation.md | 193 +- backend/requirements-new.txt | 169 + backend/requirements.txt | 3 - docker-compose.yml | 124 +- frontend/.DS_Store | Bin 8196 -> 8196 bytes frontend/app/(main)/.DS_Store | Bin 8196 -> 8196 bytes frontend/app/(main)/cart/page.tsx | 4 +- frontend/app/(main)/catalog/[slug]/page.tsx | 399 +- frontend/app/(main)/catalog/page.tsx | 61 +- frontend/app/(main)/checkout/page.tsx | 148 +- frontend/app/(main)/wishlist/page.tsx | 369 +- frontend/app/.DS_Store | Bin 6148 -> 6148 bytes frontend/app/admin/categories/page.tsx | 649 +- frontend/app/admin/dashboard/page.tsx | 280 +- frontend/app/admin/layout.tsx | 145 +- frontend/app/admin/orders/page.tsx | 15 +- frontend/app/admin/page.tsx | 4 +- frontend/app/admin/products/[id]/page.tsx | 156 +- frontend/app/admin/products/page.tsx | 63 +- frontend/app/layout.tsx | 46 +- frontend/certbot/.DS_Store | Bin 6148 -> 0 bytes frontend/components/.DS_Store | Bin 8196 -> 8196 bytes frontend/components/admin/CategoryTree.tsx | 268 + .../admin/OrderManagerOptimized.tsx | 316 + .../admin/ProductManagerOptimized.tsx | 308 + frontend/components/checkout/AddressForm.tsx | 148 - .../checkout/address-autocomplete-input.tsx | 251 + frontend/components/checkout/address-form.tsx | 618 +- .../components/checkout/cdek-widget-popup.tsx | 698 ++ .../components/checkout/order-summary.tsx | 3 +- frontend/components/product/ImageSlider.tsx | 2 +- frontend/components/product/product-card.tsx | 52 +- frontend/components/ui/data-table.tsx | 204 + frontend/components/ui/table.tsx | 5 +- frontend/hooks/use-auth.tsx | 15 - frontend/hooks/useAdminApi.ts | 369 +- frontend/hooks/useAdminCache.ts | 191 - frontend/hooks/useAdminQuery.ts | 244 + frontend/hooks/useApiRequest.ts | 271 - frontend/hooks/useCart.ts | 29 +- frontend/hooks/useCategoriesCache.ts | 33 + frontend/hooks/useMediaQuery.ts | 41 - frontend/hooks/useOrdersCache.ts | 80 + frontend/hooks/useProducts.ts | 59 + frontend/lib/api-cache.ts | 37 +- frontend/lib/api.ts | 39 +- frontend/lib/cart-store.ts | 78 +- frontend/lib/catalog-admin.ts | 16 +- frontend/lib/catalog.ts | 249 +- frontend/lib/order-service.ts | 363 +- frontend/lib/reducers/product.ts | 118 + frontend/lib/types/product.ts | 65 + frontend/package-lock.json | 105 + frontend/package.json | 5 + frontend/providers/AdminQueryProvider.tsx | 21 + frontend/providers/Providers.tsx | 27 + frontend/public/.DS_Store | Bin 6148 -> 6148 bytes frontend/types/catalog-admin.ts | 11 + frontend/types/order.ts | 88 + nginx/nginx.conf | 138 +- node_modules/.DS_Store | Bin 12292 -> 0 bytes node_modules/.package-lock.json | 285 - node_modules/asynckit/LICENSE | 21 - node_modules/asynckit/README.md | 233 - node_modules/asynckit/bench.js | 76 - node_modules/asynckit/index.js | 6 - node_modules/asynckit/lib/abort.js | 29 - node_modules/asynckit/lib/async.js | 34 - node_modules/asynckit/lib/defer.js | 26 - node_modules/asynckit/lib/iterate.js | 75 - .../asynckit/lib/readable_asynckit.js | 91 - .../asynckit/lib/readable_parallel.js | 25 - node_modules/asynckit/lib/readable_serial.js | 25 - .../asynckit/lib/readable_serial_ordered.js | 29 - node_modules/asynckit/lib/state.js | 37 - node_modules/asynckit/lib/streamify.js | 141 - node_modules/asynckit/lib/terminator.js | 29 - node_modules/asynckit/package.json | 63 - node_modules/asynckit/parallel.js | 43 - node_modules/asynckit/serial.js | 17 - node_modules/asynckit/serialOrdered.js | 75 - node_modules/asynckit/stream.js | 21 - node_modules/axios/.DS_Store | Bin 6148 -> 0 bytes node_modules/axios/CHANGELOG.md | 1158 --- node_modules/axios/LICENSE | 7 - node_modules/axios/MIGRATION_GUIDE.md | 3 - node_modules/axios/README.md | 1679 ---- node_modules/axios/dist/axios.js | 4277 --------- node_modules/axios/dist/axios.js.map | 1 - node_modules/axios/dist/axios.min.js | 3 - node_modules/axios/dist/axios.min.js.map | 1 - node_modules/axios/dist/browser/axios.cjs | 3708 ------- node_modules/axios/dist/browser/axios.cjs.map | 1 - node_modules/axios/dist/esm/axios.js | 3731 -------- node_modules/axios/dist/esm/axios.js.map | 1 - node_modules/axios/dist/esm/axios.min.js | 3 - node_modules/axios/dist/esm/axios.min.js.map | 1 - node_modules/axios/dist/node/axios.cjs | 4765 --------- node_modules/axios/dist/node/axios.cjs.map | 1 - node_modules/axios/index.d.cts | 550 -- node_modules/axios/index.d.ts | 570 -- node_modules/axios/index.js | 43 - node_modules/axios/lib/adapters/README.md | 37 - node_modules/axios/lib/adapters/adapters.js | 79 - node_modules/axios/lib/adapters/fetch.js | 229 - node_modules/axios/lib/adapters/http.js | 695 -- node_modules/axios/lib/adapters/xhr.js | 197 - node_modules/axios/lib/axios.js | 89 - node_modules/axios/lib/cancel/CancelToken.js | 135 - .../axios/lib/cancel/CanceledError.js | 25 - node_modules/axios/lib/cancel/isCancel.js | 5 - node_modules/axios/lib/core/Axios.js | 242 - node_modules/axios/lib/core/AxiosError.js | 103 - node_modules/axios/lib/core/AxiosHeaders.js | 302 - .../axios/lib/core/InterceptorManager.js | 71 - node_modules/axios/lib/core/README.md | 8 - node_modules/axios/lib/core/buildFullPath.js | 22 - .../axios/lib/core/dispatchRequest.js | 81 - node_modules/axios/lib/core/mergeConfig.js | 106 - node_modules/axios/lib/core/settle.js | 27 - node_modules/axios/lib/core/transformData.js | 28 - node_modules/axios/lib/defaults/index.js | 161 - .../axios/lib/defaults/transitional.js | 7 - node_modules/axios/lib/env/README.md | 3 - .../axios/lib/env/classes/FormData.js | 2 - node_modules/axios/lib/env/data.js | 1 - .../axios/lib/helpers/AxiosTransformStream.js | 143 - .../axios/lib/helpers/AxiosURLSearchParams.js | 58 - .../axios/lib/helpers/HttpStatusCode.js | 71 - node_modules/axios/lib/helpers/README.md | 7 - .../lib/helpers/ZlibHeaderTransformStream.js | 28 - node_modules/axios/lib/helpers/bind.js | 7 - node_modules/axios/lib/helpers/buildURL.js | 69 - node_modules/axios/lib/helpers/callbackify.js | 16 - node_modules/axios/lib/helpers/combineURLs.js | 15 - .../axios/lib/helpers/composeSignals.js | 48 - node_modules/axios/lib/helpers/cookies.js | 42 - .../axios/lib/helpers/deprecatedMethod.js | 26 - .../axios/lib/helpers/formDataToJSON.js | 95 - .../axios/lib/helpers/formDataToStream.js | 112 - node_modules/axios/lib/helpers/fromDataURI.js | 53 - .../axios/lib/helpers/isAbsoluteURL.js | 15 - .../axios/lib/helpers/isAxiosError.js | 14 - .../axios/lib/helpers/isURLSameOrigin.js | 14 - node_modules/axios/lib/helpers/null.js | 2 - .../axios/lib/helpers/parseHeaders.js | 55 - .../axios/lib/helpers/parseProtocol.js | 6 - .../axios/lib/helpers/progressEventReducer.js | 44 - node_modules/axios/lib/helpers/readBlob.js | 15 - .../axios/lib/helpers/resolveConfig.js | 57 - node_modules/axios/lib/helpers/speedometer.js | 55 - node_modules/axios/lib/helpers/spread.js | 28 - node_modules/axios/lib/helpers/throttle.js | 44 - node_modules/axios/lib/helpers/toFormData.js | 219 - .../axios/lib/helpers/toURLEncodedForm.js | 18 - node_modules/axios/lib/helpers/trackStream.js | 87 - node_modules/axios/lib/helpers/validator.js | 99 - .../lib/platform/browser/classes/Blob.js | 3 - .../lib/platform/browser/classes/FormData.js | 3 - .../browser/classes/URLSearchParams.js | 4 - .../axios/lib/platform/browser/index.js | 13 - .../axios/lib/platform/common/utils.js | 51 - node_modules/axios/lib/platform/index.js | 7 - .../lib/platform/node/classes/FormData.js | 3 - .../platform/node/classes/URLSearchParams.js | 4 - node_modules/axios/lib/platform/node/index.js | 38 - node_modules/axios/lib/utils.js | 738 -- node_modules/axios/package.json | 219 - .../call-bind-apply-helpers/.DS_Store | Bin 6148 -> 0 bytes .../call-bind-apply-helpers/.eslintrc | 17 - .../.github/FUNDING.yml | 12 - node_modules/call-bind-apply-helpers/.nycrc | 9 - .../call-bind-apply-helpers/CHANGELOG.md | 30 - node_modules/call-bind-apply-helpers/LICENSE | 21 - .../call-bind-apply-helpers/README.md | 62 - .../call-bind-apply-helpers/actualApply.d.ts | 1 - .../call-bind-apply-helpers/actualApply.js | 10 - .../call-bind-apply-helpers/applyBind.d.ts | 19 - .../call-bind-apply-helpers/applyBind.js | 10 - .../functionApply.d.ts | 1 - .../call-bind-apply-helpers/functionApply.js | 4 - .../call-bind-apply-helpers/functionCall.d.ts | 1 - .../call-bind-apply-helpers/functionCall.js | 4 - .../call-bind-apply-helpers/index.d.ts | 64 - node_modules/call-bind-apply-helpers/index.js | 15 - .../call-bind-apply-helpers/package.json | 85 - .../call-bind-apply-helpers/reflectApply.d.ts | 3 - .../call-bind-apply-helpers/reflectApply.js | 4 - .../call-bind-apply-helpers/test/index.js | 63 - .../call-bind-apply-helpers/tsconfig.json | 9 - node_modules/combined-stream/License | 19 - node_modules/combined-stream/Readme.md | 138 - .../combined-stream/lib/combined_stream.js | 208 - node_modules/combined-stream/package.json | 25 - node_modules/combined-stream/yarn.lock | 17 - node_modules/delayed-stream/.npmignore | 1 - node_modules/delayed-stream/License | 19 - node_modules/delayed-stream/Makefile | 7 - node_modules/delayed-stream/Readme.md | 141 - .../delayed-stream/lib/delayed_stream.js | 107 - node_modules/delayed-stream/package.json | 27 - node_modules/dunder-proto/.DS_Store | Bin 6148 -> 0 bytes node_modules/dunder-proto/.eslintrc | 5 - node_modules/dunder-proto/.github/FUNDING.yml | 12 - node_modules/dunder-proto/.nycrc | 13 - node_modules/dunder-proto/CHANGELOG.md | 24 - node_modules/dunder-proto/LICENSE | 21 - node_modules/dunder-proto/README.md | 54 - node_modules/dunder-proto/get.d.ts | 5 - node_modules/dunder-proto/get.js | 30 - node_modules/dunder-proto/package.json | 76 - node_modules/dunder-proto/set.d.ts | 5 - node_modules/dunder-proto/set.js | 35 - node_modules/dunder-proto/test/get.js | 34 - node_modules/dunder-proto/test/index.js | 4 - node_modules/dunder-proto/test/set.js | 50 - node_modules/dunder-proto/tsconfig.json | 9 - node_modules/es-define-property/.DS_Store | Bin 6148 -> 0 bytes node_modules/es-define-property/.eslintrc | 13 - .../es-define-property/.github/FUNDING.yml | 12 - node_modules/es-define-property/.nycrc | 9 - node_modules/es-define-property/CHANGELOG.md | 29 - node_modules/es-define-property/LICENSE | 21 - node_modules/es-define-property/README.md | 49 - node_modules/es-define-property/index.d.ts | 3 - node_modules/es-define-property/index.js | 14 - node_modules/es-define-property/package.json | 81 - node_modules/es-define-property/test/index.js | 56 - node_modules/es-define-property/tsconfig.json | 10 - node_modules/es-errors/.DS_Store | Bin 6148 -> 0 bytes node_modules/es-errors/.eslintrc | 5 - node_modules/es-errors/.github/FUNDING.yml | 12 - node_modules/es-errors/CHANGELOG.md | 40 - node_modules/es-errors/LICENSE | 21 - node_modules/es-errors/README.md | 55 - node_modules/es-errors/eval.d.ts | 3 - node_modules/es-errors/eval.js | 4 - node_modules/es-errors/index.d.ts | 3 - node_modules/es-errors/index.js | 4 - node_modules/es-errors/package.json | 80 - node_modules/es-errors/range.d.ts | 3 - node_modules/es-errors/range.js | 4 - node_modules/es-errors/ref.d.ts | 3 - node_modules/es-errors/ref.js | 4 - node_modules/es-errors/syntax.d.ts | 3 - node_modules/es-errors/syntax.js | 4 - node_modules/es-errors/test/index.js | 19 - node_modules/es-errors/tsconfig.json | 49 - node_modules/es-errors/type.d.ts | 3 - node_modules/es-errors/type.js | 4 - node_modules/es-errors/uri.d.ts | 3 - node_modules/es-errors/uri.js | 4 - node_modules/es-object-atoms/.DS_Store | Bin 6148 -> 0 bytes node_modules/es-object-atoms/.eslintrc | 16 - .../es-object-atoms/.github/FUNDING.yml | 12 - node_modules/es-object-atoms/CHANGELOG.md | 37 - node_modules/es-object-atoms/LICENSE | 21 - node_modules/es-object-atoms/README.md | 63 - .../RequireObjectCoercible.d.ts | 3 - .../es-object-atoms/RequireObjectCoercible.js | 11 - node_modules/es-object-atoms/ToObject.d.ts | 7 - node_modules/es-object-atoms/ToObject.js | 10 - node_modules/es-object-atoms/index.d.ts | 3 - node_modules/es-object-atoms/index.js | 4 - node_modules/es-object-atoms/isObject.d.ts | 3 - node_modules/es-object-atoms/isObject.js | 6 - node_modules/es-object-atoms/package.json | 80 - node_modules/es-object-atoms/test/index.js | 38 - node_modules/es-object-atoms/tsconfig.json | 6 - node_modules/es-set-tostringtag/.eslintrc | 13 - node_modules/es-set-tostringtag/.nycrc | 9 - node_modules/es-set-tostringtag/CHANGELOG.md | 67 - node_modules/es-set-tostringtag/LICENSE | 21 - node_modules/es-set-tostringtag/README.md | 53 - node_modules/es-set-tostringtag/index.d.ts | 10 - node_modules/es-set-tostringtag/index.js | 35 - node_modules/es-set-tostringtag/package.json | 78 - node_modules/es-set-tostringtag/test/index.js | 85 - node_modules/es-set-tostringtag/tsconfig.json | 9 - node_modules/follow-redirects/LICENSE | 18 - node_modules/follow-redirects/README.md | 155 - node_modules/follow-redirects/debug.js | 15 - node_modules/follow-redirects/http.js | 1 - node_modules/follow-redirects/https.js | 1 - node_modules/follow-redirects/index.js | 686 -- node_modules/follow-redirects/package.json | 58 - node_modules/form-data/License | 19 - node_modules/form-data/Readme.md | 358 - node_modules/form-data/index.d.ts | 62 - node_modules/form-data/lib/browser.js | 2 - node_modules/form-data/lib/form_data.js | 503 - node_modules/form-data/lib/populate.js | 10 - node_modules/form-data/package.json | 74 - node_modules/function-bind/.DS_Store | Bin 6148 -> 0 bytes node_modules/function-bind/.eslintrc | 21 - .../function-bind/.github/FUNDING.yml | 12 - .../function-bind/.github/SECURITY.md | 3 - node_modules/function-bind/.nycrc | 13 - node_modules/function-bind/CHANGELOG.md | 136 - node_modules/function-bind/LICENSE | 20 - node_modules/function-bind/README.md | 46 - node_modules/function-bind/implementation.js | 84 - node_modules/function-bind/index.js | 5 - node_modules/function-bind/package.json | 87 - node_modules/function-bind/test/.eslintrc | 9 - node_modules/function-bind/test/index.js | 252 - node_modules/get-intrinsic/.DS_Store | Bin 6148 -> 0 bytes node_modules/get-intrinsic/.eslintrc | 42 - .../get-intrinsic/.github/FUNDING.yml | 12 - node_modules/get-intrinsic/.nycrc | 9 - node_modules/get-intrinsic/CHANGELOG.md | 186 - node_modules/get-intrinsic/LICENSE | 21 - node_modules/get-intrinsic/README.md | 71 - node_modules/get-intrinsic/index.js | 378 - node_modules/get-intrinsic/package.json | 97 - .../get-intrinsic/test/GetIntrinsic.js | 274 - node_modules/get-proto/.DS_Store | Bin 6148 -> 0 bytes node_modules/get-proto/.eslintrc | 10 - node_modules/get-proto/.github/FUNDING.yml | 12 - node_modules/get-proto/.nycrc | 9 - node_modules/get-proto/CHANGELOG.md | 21 - node_modules/get-proto/LICENSE | 21 - .../get-proto/Object.getPrototypeOf.d.ts | 5 - .../get-proto/Object.getPrototypeOf.js | 6 - node_modules/get-proto/README.md | 50 - .../get-proto/Reflect.getPrototypeOf.d.ts | 3 - .../get-proto/Reflect.getPrototypeOf.js | 4 - node_modules/get-proto/index.d.ts | 5 - node_modules/get-proto/index.js | 27 - node_modules/get-proto/package.json | 81 - node_modules/get-proto/test/index.js | 68 - node_modules/get-proto/tsconfig.json | 9 - node_modules/gopd/.DS_Store | Bin 6148 -> 0 bytes node_modules/gopd/.eslintrc | 16 - node_modules/gopd/.github/FUNDING.yml | 12 - node_modules/gopd/CHANGELOG.md | 45 - node_modules/gopd/LICENSE | 21 - node_modules/gopd/README.md | 40 - node_modules/gopd/gOPD.d.ts | 1 - node_modules/gopd/gOPD.js | 4 - node_modules/gopd/index.d.ts | 5 - node_modules/gopd/index.js | 15 - node_modules/gopd/package.json | 77 - node_modules/gopd/test/index.js | 36 - node_modules/gopd/tsconfig.json | 9 - node_modules/has-symbols/.DS_Store | Bin 6148 -> 0 bytes node_modules/has-symbols/.eslintrc | 11 - node_modules/has-symbols/.github/FUNDING.yml | 12 - node_modules/has-symbols/.nycrc | 9 - node_modules/has-symbols/CHANGELOG.md | 91 - node_modules/has-symbols/LICENSE | 21 - node_modules/has-symbols/README.md | 46 - node_modules/has-symbols/index.d.ts | 3 - node_modules/has-symbols/index.js | 14 - node_modules/has-symbols/package.json | 111 - node_modules/has-symbols/shams.d.ts | 3 - node_modules/has-symbols/shams.js | 45 - node_modules/has-symbols/test/index.js | 22 - .../has-symbols/test/shams/core-js.js | 29 - .../test/shams/get-own-property-symbols.js | 29 - node_modules/has-symbols/test/tests.js | 58 - node_modules/has-symbols/tsconfig.json | 10 - node_modules/has-tostringtag/.DS_Store | Bin 6148 -> 0 bytes node_modules/has-tostringtag/.eslintrc | 5 - .../has-tostringtag/.github/FUNDING.yml | 12 - node_modules/has-tostringtag/.nycrc | 13 - node_modules/has-tostringtag/CHANGELOG.md | 42 - node_modules/has-tostringtag/LICENSE | 21 - node_modules/has-tostringtag/README.md | 46 - node_modules/has-tostringtag/index.d.ts | 3 - node_modules/has-tostringtag/index.js | 8 - node_modules/has-tostringtag/package.json | 108 - node_modules/has-tostringtag/shams.d.ts | 3 - node_modules/has-tostringtag/shams.js | 8 - node_modules/has-tostringtag/test/index.js | 21 - .../has-tostringtag/test/shams/core-js.js | 31 - .../test/shams/get-own-property-symbols.js | 30 - node_modules/has-tostringtag/test/tests.js | 15 - node_modules/has-tostringtag/tsconfig.json | 49 - node_modules/hasown/.eslintrc | 5 - node_modules/hasown/.github/FUNDING.yml | 12 - node_modules/hasown/.nycrc | 13 - node_modules/hasown/CHANGELOG.md | 40 - node_modules/hasown/LICENSE | 21 - node_modules/hasown/README.md | 40 - node_modules/hasown/index.d.ts | 3 - node_modules/hasown/index.js | 8 - node_modules/hasown/package.json | 92 - node_modules/hasown/tsconfig.json | 6 - node_modules/math-intrinsics/.DS_Store | Bin 6148 -> 0 bytes node_modules/math-intrinsics/.eslintrc | 16 - .../math-intrinsics/.github/FUNDING.yml | 12 - node_modules/math-intrinsics/CHANGELOG.md | 24 - node_modules/math-intrinsics/LICENSE | 21 - node_modules/math-intrinsics/README.md | 50 - node_modules/math-intrinsics/abs.d.ts | 1 - node_modules/math-intrinsics/abs.js | 4 - .../constants/maxArrayLength.d.ts | 3 - .../constants/maxArrayLength.js | 4 - .../constants/maxSafeInteger.d.ts | 3 - .../constants/maxSafeInteger.js | 5 - .../math-intrinsics/constants/maxValue.d.ts | 3 - .../math-intrinsics/constants/maxValue.js | 5 - node_modules/math-intrinsics/floor.d.ts | 1 - node_modules/math-intrinsics/floor.js | 4 - node_modules/math-intrinsics/isFinite.d.ts | 3 - node_modules/math-intrinsics/isFinite.js | 12 - node_modules/math-intrinsics/isInteger.d.ts | 3 - node_modules/math-intrinsics/isInteger.js | 16 - node_modules/math-intrinsics/isNaN.d.ts | 1 - node_modules/math-intrinsics/isNaN.js | 6 - .../math-intrinsics/isNegativeZero.d.ts | 3 - .../math-intrinsics/isNegativeZero.js | 6 - node_modules/math-intrinsics/max.d.ts | 1 - node_modules/math-intrinsics/max.js | 4 - node_modules/math-intrinsics/min.d.ts | 1 - node_modules/math-intrinsics/min.js | 4 - node_modules/math-intrinsics/mod.d.ts | 3 - node_modules/math-intrinsics/mod.js | 9 - node_modules/math-intrinsics/package.json | 86 - node_modules/math-intrinsics/pow.d.ts | 1 - node_modules/math-intrinsics/pow.js | 4 - node_modules/math-intrinsics/round.d.ts | 1 - node_modules/math-intrinsics/round.js | 4 - node_modules/math-intrinsics/sign.d.ts | 3 - node_modules/math-intrinsics/sign.js | 11 - node_modules/math-intrinsics/test/index.js | 192 - node_modules/math-intrinsics/tsconfig.json | 3 - node_modules/mime-db/HISTORY.md | 507 - node_modules/mime-db/LICENSE | 23 - node_modules/mime-db/README.md | 100 - node_modules/mime-db/db.json | 8519 ----------------- node_modules/mime-db/index.js | 12 - node_modules/mime-db/package.json | 60 - node_modules/mime-types/HISTORY.md | 397 - node_modules/mime-types/LICENSE | 23 - node_modules/mime-types/README.md | 113 - node_modules/mime-types/index.js | 188 - node_modules/mime-types/package.json | 44 - node_modules/proxy-from-env/.eslintrc | 29 - node_modules/proxy-from-env/.travis.yml | 10 - node_modules/proxy-from-env/LICENSE | 20 - node_modules/proxy-from-env/README.md | 131 - node_modules/proxy-from-env/index.js | 108 - node_modules/proxy-from-env/package.json | 34 - node_modules/proxy-from-env/test.js | 483 - package-lock.json | 290 - package.json | 5 - php/service.php | 376 + test.txt | 1 - test/test.jpg | 1 - 490 files changed, 6828 insertions(+), 50254 deletions(-) create mode 100644 .gitignore create mode 100644 backend/Dockerfile create mode 100644 backend/alembic/versions/7192b0707277_add_new_order.py create mode 100644 backend/alembic/versions/__pycache__/7192b0707277_add_new_order.cpython-310.pyc create mode 100644 backend/alembic/versions/__pycache__/a81393a28fee_add_new_order_.cpython-310.pyc create mode 100644 backend/alembic/versions/__pycache__/f89a59b0e814_add_new_order_.cpython-310.pyc create mode 100644 backend/alembic/versions/f89a59b0e814_add_new_order_.py create mode 100644 backend/requirements-new.txt delete mode 100644 frontend/certbot/.DS_Store create mode 100644 frontend/components/admin/CategoryTree.tsx create mode 100644 frontend/components/admin/OrderManagerOptimized.tsx create mode 100644 frontend/components/admin/ProductManagerOptimized.tsx delete mode 100644 frontend/components/checkout/AddressForm.tsx create mode 100644 frontend/components/checkout/address-autocomplete-input.tsx create mode 100644 frontend/components/checkout/cdek-widget-popup.tsx create mode 100644 frontend/components/ui/data-table.tsx delete mode 100644 frontend/hooks/use-auth.tsx delete mode 100644 frontend/hooks/useAdminCache.ts create mode 100644 frontend/hooks/useAdminQuery.ts delete mode 100644 frontend/hooks/useApiRequest.ts create mode 100644 frontend/hooks/useCategoriesCache.ts delete mode 100644 frontend/hooks/useMediaQuery.ts create mode 100644 frontend/hooks/useOrdersCache.ts create mode 100644 frontend/hooks/useProducts.ts create mode 100644 frontend/lib/reducers/product.ts create mode 100644 frontend/lib/types/product.ts create mode 100644 frontend/providers/AdminQueryProvider.tsx create mode 100644 frontend/providers/Providers.tsx create mode 100644 frontend/types/catalog-admin.ts create mode 100644 frontend/types/order.ts delete mode 100644 node_modules/.DS_Store delete mode 100644 node_modules/.package-lock.json delete mode 100644 node_modules/asynckit/LICENSE delete mode 100644 node_modules/asynckit/README.md delete mode 100644 node_modules/asynckit/bench.js delete mode 100644 node_modules/asynckit/index.js delete mode 100644 node_modules/asynckit/lib/abort.js delete mode 100644 node_modules/asynckit/lib/async.js delete mode 100644 node_modules/asynckit/lib/defer.js delete mode 100644 node_modules/asynckit/lib/iterate.js delete mode 100644 node_modules/asynckit/lib/readable_asynckit.js delete mode 100644 node_modules/asynckit/lib/readable_parallel.js delete mode 100644 node_modules/asynckit/lib/readable_serial.js delete mode 100644 node_modules/asynckit/lib/readable_serial_ordered.js delete mode 100644 node_modules/asynckit/lib/state.js delete mode 100644 node_modules/asynckit/lib/streamify.js delete mode 100644 node_modules/asynckit/lib/terminator.js delete mode 100644 node_modules/asynckit/package.json delete mode 100644 node_modules/asynckit/parallel.js delete mode 100644 node_modules/asynckit/serial.js delete mode 100644 node_modules/asynckit/serialOrdered.js delete mode 100644 node_modules/asynckit/stream.js delete mode 100644 node_modules/axios/.DS_Store delete mode 100644 node_modules/axios/CHANGELOG.md delete mode 100644 node_modules/axios/LICENSE delete mode 100644 node_modules/axios/MIGRATION_GUIDE.md delete mode 100644 node_modules/axios/README.md delete mode 100644 node_modules/axios/dist/axios.js delete mode 100644 node_modules/axios/dist/axios.js.map delete mode 100644 node_modules/axios/dist/axios.min.js delete mode 100644 node_modules/axios/dist/axios.min.js.map delete mode 100644 node_modules/axios/dist/browser/axios.cjs delete mode 100644 node_modules/axios/dist/browser/axios.cjs.map delete mode 100644 node_modules/axios/dist/esm/axios.js delete mode 100644 node_modules/axios/dist/esm/axios.js.map delete mode 100644 node_modules/axios/dist/esm/axios.min.js delete mode 100644 node_modules/axios/dist/esm/axios.min.js.map delete mode 100644 node_modules/axios/dist/node/axios.cjs delete mode 100644 node_modules/axios/dist/node/axios.cjs.map delete mode 100644 node_modules/axios/index.d.cts delete mode 100644 node_modules/axios/index.d.ts delete mode 100644 node_modules/axios/index.js delete mode 100644 node_modules/axios/lib/adapters/README.md delete mode 100644 node_modules/axios/lib/adapters/adapters.js delete mode 100644 node_modules/axios/lib/adapters/fetch.js delete mode 100755 node_modules/axios/lib/adapters/http.js delete mode 100644 node_modules/axios/lib/adapters/xhr.js delete mode 100644 node_modules/axios/lib/axios.js delete mode 100644 node_modules/axios/lib/cancel/CancelToken.js delete mode 100644 node_modules/axios/lib/cancel/CanceledError.js delete mode 100644 node_modules/axios/lib/cancel/isCancel.js delete mode 100644 node_modules/axios/lib/core/Axios.js delete mode 100644 node_modules/axios/lib/core/AxiosError.js delete mode 100644 node_modules/axios/lib/core/AxiosHeaders.js delete mode 100644 node_modules/axios/lib/core/InterceptorManager.js delete mode 100644 node_modules/axios/lib/core/README.md delete mode 100644 node_modules/axios/lib/core/buildFullPath.js delete mode 100644 node_modules/axios/lib/core/dispatchRequest.js delete mode 100644 node_modules/axios/lib/core/mergeConfig.js delete mode 100644 node_modules/axios/lib/core/settle.js delete mode 100644 node_modules/axios/lib/core/transformData.js delete mode 100644 node_modules/axios/lib/defaults/index.js delete mode 100644 node_modules/axios/lib/defaults/transitional.js delete mode 100644 node_modules/axios/lib/env/README.md delete mode 100644 node_modules/axios/lib/env/classes/FormData.js delete mode 100644 node_modules/axios/lib/env/data.js delete mode 100644 node_modules/axios/lib/helpers/AxiosTransformStream.js delete mode 100644 node_modules/axios/lib/helpers/AxiosURLSearchParams.js delete mode 100644 node_modules/axios/lib/helpers/HttpStatusCode.js delete mode 100644 node_modules/axios/lib/helpers/README.md delete mode 100644 node_modules/axios/lib/helpers/ZlibHeaderTransformStream.js delete mode 100644 node_modules/axios/lib/helpers/bind.js delete mode 100644 node_modules/axios/lib/helpers/buildURL.js delete mode 100644 node_modules/axios/lib/helpers/callbackify.js delete mode 100644 node_modules/axios/lib/helpers/combineURLs.js delete mode 100644 node_modules/axios/lib/helpers/composeSignals.js delete mode 100644 node_modules/axios/lib/helpers/cookies.js delete mode 100644 node_modules/axios/lib/helpers/deprecatedMethod.js delete mode 100644 node_modules/axios/lib/helpers/formDataToJSON.js delete mode 100644 node_modules/axios/lib/helpers/formDataToStream.js delete mode 100644 node_modules/axios/lib/helpers/fromDataURI.js delete mode 100644 node_modules/axios/lib/helpers/isAbsoluteURL.js delete mode 100644 node_modules/axios/lib/helpers/isAxiosError.js delete mode 100644 node_modules/axios/lib/helpers/isURLSameOrigin.js delete mode 100644 node_modules/axios/lib/helpers/null.js delete mode 100644 node_modules/axios/lib/helpers/parseHeaders.js delete mode 100644 node_modules/axios/lib/helpers/parseProtocol.js delete mode 100644 node_modules/axios/lib/helpers/progressEventReducer.js delete mode 100644 node_modules/axios/lib/helpers/readBlob.js delete mode 100644 node_modules/axios/lib/helpers/resolveConfig.js delete mode 100644 node_modules/axios/lib/helpers/speedometer.js delete mode 100644 node_modules/axios/lib/helpers/spread.js delete mode 100644 node_modules/axios/lib/helpers/throttle.js delete mode 100644 node_modules/axios/lib/helpers/toFormData.js delete mode 100644 node_modules/axios/lib/helpers/toURLEncodedForm.js delete mode 100644 node_modules/axios/lib/helpers/trackStream.js delete mode 100644 node_modules/axios/lib/helpers/validator.js delete mode 100644 node_modules/axios/lib/platform/browser/classes/Blob.js delete mode 100644 node_modules/axios/lib/platform/browser/classes/FormData.js delete mode 100644 node_modules/axios/lib/platform/browser/classes/URLSearchParams.js delete mode 100644 node_modules/axios/lib/platform/browser/index.js delete mode 100644 node_modules/axios/lib/platform/common/utils.js delete mode 100644 node_modules/axios/lib/platform/index.js delete mode 100644 node_modules/axios/lib/platform/node/classes/FormData.js delete mode 100644 node_modules/axios/lib/platform/node/classes/URLSearchParams.js delete mode 100644 node_modules/axios/lib/platform/node/index.js delete mode 100644 node_modules/axios/lib/utils.js delete mode 100644 node_modules/axios/package.json delete mode 100644 node_modules/call-bind-apply-helpers/.DS_Store delete mode 100644 node_modules/call-bind-apply-helpers/.eslintrc delete mode 100644 node_modules/call-bind-apply-helpers/.github/FUNDING.yml delete mode 100644 node_modules/call-bind-apply-helpers/.nycrc delete mode 100644 node_modules/call-bind-apply-helpers/CHANGELOG.md delete mode 100644 node_modules/call-bind-apply-helpers/LICENSE delete mode 100644 node_modules/call-bind-apply-helpers/README.md delete mode 100644 node_modules/call-bind-apply-helpers/actualApply.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/actualApply.js delete mode 100644 node_modules/call-bind-apply-helpers/applyBind.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/applyBind.js delete mode 100644 node_modules/call-bind-apply-helpers/functionApply.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/functionApply.js delete mode 100644 node_modules/call-bind-apply-helpers/functionCall.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/functionCall.js delete mode 100644 node_modules/call-bind-apply-helpers/index.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/index.js delete mode 100644 node_modules/call-bind-apply-helpers/package.json delete mode 100644 node_modules/call-bind-apply-helpers/reflectApply.d.ts delete mode 100644 node_modules/call-bind-apply-helpers/reflectApply.js delete mode 100644 node_modules/call-bind-apply-helpers/test/index.js delete mode 100644 node_modules/call-bind-apply-helpers/tsconfig.json delete mode 100644 node_modules/combined-stream/License delete mode 100644 node_modules/combined-stream/Readme.md delete mode 100644 node_modules/combined-stream/lib/combined_stream.js delete mode 100644 node_modules/combined-stream/package.json delete mode 100644 node_modules/combined-stream/yarn.lock delete mode 100644 node_modules/delayed-stream/.npmignore delete mode 100644 node_modules/delayed-stream/License delete mode 100644 node_modules/delayed-stream/Makefile delete mode 100644 node_modules/delayed-stream/Readme.md delete mode 100644 node_modules/delayed-stream/lib/delayed_stream.js delete mode 100644 node_modules/delayed-stream/package.json delete mode 100644 node_modules/dunder-proto/.DS_Store delete mode 100644 node_modules/dunder-proto/.eslintrc delete mode 100644 node_modules/dunder-proto/.github/FUNDING.yml delete mode 100644 node_modules/dunder-proto/.nycrc delete mode 100644 node_modules/dunder-proto/CHANGELOG.md delete mode 100644 node_modules/dunder-proto/LICENSE delete mode 100644 node_modules/dunder-proto/README.md delete mode 100644 node_modules/dunder-proto/get.d.ts delete mode 100644 node_modules/dunder-proto/get.js delete mode 100644 node_modules/dunder-proto/package.json delete mode 100644 node_modules/dunder-proto/set.d.ts delete mode 100644 node_modules/dunder-proto/set.js delete mode 100644 node_modules/dunder-proto/test/get.js delete mode 100644 node_modules/dunder-proto/test/index.js delete mode 100644 node_modules/dunder-proto/test/set.js delete mode 100644 node_modules/dunder-proto/tsconfig.json delete mode 100644 node_modules/es-define-property/.DS_Store delete mode 100644 node_modules/es-define-property/.eslintrc delete mode 100644 node_modules/es-define-property/.github/FUNDING.yml delete mode 100644 node_modules/es-define-property/.nycrc delete mode 100644 node_modules/es-define-property/CHANGELOG.md delete mode 100644 node_modules/es-define-property/LICENSE delete mode 100644 node_modules/es-define-property/README.md delete mode 100644 node_modules/es-define-property/index.d.ts delete mode 100644 node_modules/es-define-property/index.js delete mode 100644 node_modules/es-define-property/package.json delete mode 100644 node_modules/es-define-property/test/index.js delete mode 100644 node_modules/es-define-property/tsconfig.json delete mode 100644 node_modules/es-errors/.DS_Store delete mode 100644 node_modules/es-errors/.eslintrc delete mode 100644 node_modules/es-errors/.github/FUNDING.yml delete mode 100644 node_modules/es-errors/CHANGELOG.md delete mode 100644 node_modules/es-errors/LICENSE delete mode 100644 node_modules/es-errors/README.md delete mode 100644 node_modules/es-errors/eval.d.ts delete mode 100644 node_modules/es-errors/eval.js delete mode 100644 node_modules/es-errors/index.d.ts delete mode 100644 node_modules/es-errors/index.js delete mode 100644 node_modules/es-errors/package.json delete mode 100644 node_modules/es-errors/range.d.ts delete mode 100644 node_modules/es-errors/range.js delete mode 100644 node_modules/es-errors/ref.d.ts delete mode 100644 node_modules/es-errors/ref.js delete mode 100644 node_modules/es-errors/syntax.d.ts delete mode 100644 node_modules/es-errors/syntax.js delete mode 100644 node_modules/es-errors/test/index.js delete mode 100644 node_modules/es-errors/tsconfig.json delete mode 100644 node_modules/es-errors/type.d.ts delete mode 100644 node_modules/es-errors/type.js delete mode 100644 node_modules/es-errors/uri.d.ts delete mode 100644 node_modules/es-errors/uri.js delete mode 100644 node_modules/es-object-atoms/.DS_Store delete mode 100644 node_modules/es-object-atoms/.eslintrc delete mode 100644 node_modules/es-object-atoms/.github/FUNDING.yml delete mode 100644 node_modules/es-object-atoms/CHANGELOG.md delete mode 100644 node_modules/es-object-atoms/LICENSE delete mode 100644 node_modules/es-object-atoms/README.md delete mode 100644 node_modules/es-object-atoms/RequireObjectCoercible.d.ts delete mode 100644 node_modules/es-object-atoms/RequireObjectCoercible.js delete mode 100644 node_modules/es-object-atoms/ToObject.d.ts delete mode 100644 node_modules/es-object-atoms/ToObject.js delete mode 100644 node_modules/es-object-atoms/index.d.ts delete mode 100644 node_modules/es-object-atoms/index.js delete mode 100644 node_modules/es-object-atoms/isObject.d.ts delete mode 100644 node_modules/es-object-atoms/isObject.js delete mode 100644 node_modules/es-object-atoms/package.json delete mode 100644 node_modules/es-object-atoms/test/index.js delete mode 100644 node_modules/es-object-atoms/tsconfig.json delete mode 100644 node_modules/es-set-tostringtag/.eslintrc delete mode 100644 node_modules/es-set-tostringtag/.nycrc delete mode 100644 node_modules/es-set-tostringtag/CHANGELOG.md delete mode 100644 node_modules/es-set-tostringtag/LICENSE delete mode 100644 node_modules/es-set-tostringtag/README.md delete mode 100644 node_modules/es-set-tostringtag/index.d.ts delete mode 100644 node_modules/es-set-tostringtag/index.js delete mode 100644 node_modules/es-set-tostringtag/package.json delete mode 100644 node_modules/es-set-tostringtag/test/index.js delete mode 100644 node_modules/es-set-tostringtag/tsconfig.json delete mode 100644 node_modules/follow-redirects/LICENSE delete mode 100644 node_modules/follow-redirects/README.md delete mode 100644 node_modules/follow-redirects/debug.js delete mode 100644 node_modules/follow-redirects/http.js delete mode 100644 node_modules/follow-redirects/https.js delete mode 100644 node_modules/follow-redirects/index.js delete mode 100644 node_modules/follow-redirects/package.json delete mode 100644 node_modules/form-data/License delete mode 100644 node_modules/form-data/Readme.md delete mode 100644 node_modules/form-data/index.d.ts delete mode 100644 node_modules/form-data/lib/browser.js delete mode 100644 node_modules/form-data/lib/form_data.js delete mode 100644 node_modules/form-data/lib/populate.js delete mode 100644 node_modules/form-data/package.json delete mode 100644 node_modules/function-bind/.DS_Store delete mode 100644 node_modules/function-bind/.eslintrc delete mode 100644 node_modules/function-bind/.github/FUNDING.yml delete mode 100644 node_modules/function-bind/.github/SECURITY.md delete mode 100644 node_modules/function-bind/.nycrc delete mode 100644 node_modules/function-bind/CHANGELOG.md delete mode 100644 node_modules/function-bind/LICENSE delete mode 100644 node_modules/function-bind/README.md delete mode 100644 node_modules/function-bind/implementation.js delete mode 100644 node_modules/function-bind/index.js delete mode 100644 node_modules/function-bind/package.json delete mode 100644 node_modules/function-bind/test/.eslintrc delete mode 100644 node_modules/function-bind/test/index.js delete mode 100644 node_modules/get-intrinsic/.DS_Store delete mode 100644 node_modules/get-intrinsic/.eslintrc delete mode 100644 node_modules/get-intrinsic/.github/FUNDING.yml delete mode 100644 node_modules/get-intrinsic/.nycrc delete mode 100644 node_modules/get-intrinsic/CHANGELOG.md delete mode 100644 node_modules/get-intrinsic/LICENSE delete mode 100644 node_modules/get-intrinsic/README.md delete mode 100644 node_modules/get-intrinsic/index.js delete mode 100644 node_modules/get-intrinsic/package.json delete mode 100644 node_modules/get-intrinsic/test/GetIntrinsic.js delete mode 100644 node_modules/get-proto/.DS_Store delete mode 100644 node_modules/get-proto/.eslintrc delete mode 100644 node_modules/get-proto/.github/FUNDING.yml delete mode 100644 node_modules/get-proto/.nycrc delete mode 100644 node_modules/get-proto/CHANGELOG.md delete mode 100644 node_modules/get-proto/LICENSE delete mode 100644 node_modules/get-proto/Object.getPrototypeOf.d.ts delete mode 100644 node_modules/get-proto/Object.getPrototypeOf.js delete mode 100644 node_modules/get-proto/README.md delete mode 100644 node_modules/get-proto/Reflect.getPrototypeOf.d.ts delete mode 100644 node_modules/get-proto/Reflect.getPrototypeOf.js delete mode 100644 node_modules/get-proto/index.d.ts delete mode 100644 node_modules/get-proto/index.js delete mode 100644 node_modules/get-proto/package.json delete mode 100644 node_modules/get-proto/test/index.js delete mode 100644 node_modules/get-proto/tsconfig.json delete mode 100644 node_modules/gopd/.DS_Store delete mode 100644 node_modules/gopd/.eslintrc delete mode 100644 node_modules/gopd/.github/FUNDING.yml delete mode 100644 node_modules/gopd/CHANGELOG.md delete mode 100644 node_modules/gopd/LICENSE delete mode 100644 node_modules/gopd/README.md delete mode 100644 node_modules/gopd/gOPD.d.ts delete mode 100644 node_modules/gopd/gOPD.js delete mode 100644 node_modules/gopd/index.d.ts delete mode 100644 node_modules/gopd/index.js delete mode 100644 node_modules/gopd/package.json delete mode 100644 node_modules/gopd/test/index.js delete mode 100644 node_modules/gopd/tsconfig.json delete mode 100644 node_modules/has-symbols/.DS_Store delete mode 100644 node_modules/has-symbols/.eslintrc delete mode 100644 node_modules/has-symbols/.github/FUNDING.yml delete mode 100644 node_modules/has-symbols/.nycrc delete mode 100644 node_modules/has-symbols/CHANGELOG.md delete mode 100644 node_modules/has-symbols/LICENSE delete mode 100644 node_modules/has-symbols/README.md delete mode 100644 node_modules/has-symbols/index.d.ts delete mode 100644 node_modules/has-symbols/index.js delete mode 100644 node_modules/has-symbols/package.json delete mode 100644 node_modules/has-symbols/shams.d.ts delete mode 100644 node_modules/has-symbols/shams.js delete mode 100644 node_modules/has-symbols/test/index.js delete mode 100644 node_modules/has-symbols/test/shams/core-js.js delete mode 100644 node_modules/has-symbols/test/shams/get-own-property-symbols.js delete mode 100644 node_modules/has-symbols/test/tests.js delete mode 100644 node_modules/has-symbols/tsconfig.json delete mode 100644 node_modules/has-tostringtag/.DS_Store delete mode 100644 node_modules/has-tostringtag/.eslintrc delete mode 100644 node_modules/has-tostringtag/.github/FUNDING.yml delete mode 100644 node_modules/has-tostringtag/.nycrc delete mode 100644 node_modules/has-tostringtag/CHANGELOG.md delete mode 100644 node_modules/has-tostringtag/LICENSE delete mode 100644 node_modules/has-tostringtag/README.md delete mode 100644 node_modules/has-tostringtag/index.d.ts delete mode 100644 node_modules/has-tostringtag/index.js delete mode 100644 node_modules/has-tostringtag/package.json delete mode 100644 node_modules/has-tostringtag/shams.d.ts delete mode 100644 node_modules/has-tostringtag/shams.js delete mode 100644 node_modules/has-tostringtag/test/index.js delete mode 100644 node_modules/has-tostringtag/test/shams/core-js.js delete mode 100644 node_modules/has-tostringtag/test/shams/get-own-property-symbols.js delete mode 100644 node_modules/has-tostringtag/test/tests.js delete mode 100644 node_modules/has-tostringtag/tsconfig.json delete mode 100644 node_modules/hasown/.eslintrc delete mode 100644 node_modules/hasown/.github/FUNDING.yml delete mode 100644 node_modules/hasown/.nycrc delete mode 100644 node_modules/hasown/CHANGELOG.md delete mode 100644 node_modules/hasown/LICENSE delete mode 100644 node_modules/hasown/README.md delete mode 100644 node_modules/hasown/index.d.ts delete mode 100644 node_modules/hasown/index.js delete mode 100644 node_modules/hasown/package.json delete mode 100644 node_modules/hasown/tsconfig.json delete mode 100644 node_modules/math-intrinsics/.DS_Store delete mode 100644 node_modules/math-intrinsics/.eslintrc delete mode 100644 node_modules/math-intrinsics/.github/FUNDING.yml delete mode 100644 node_modules/math-intrinsics/CHANGELOG.md delete mode 100644 node_modules/math-intrinsics/LICENSE delete mode 100644 node_modules/math-intrinsics/README.md delete mode 100644 node_modules/math-intrinsics/abs.d.ts delete mode 100644 node_modules/math-intrinsics/abs.js delete mode 100644 node_modules/math-intrinsics/constants/maxArrayLength.d.ts delete mode 100644 node_modules/math-intrinsics/constants/maxArrayLength.js delete mode 100644 node_modules/math-intrinsics/constants/maxSafeInteger.d.ts delete mode 100644 node_modules/math-intrinsics/constants/maxSafeInteger.js delete mode 100644 node_modules/math-intrinsics/constants/maxValue.d.ts delete mode 100644 node_modules/math-intrinsics/constants/maxValue.js delete mode 100644 node_modules/math-intrinsics/floor.d.ts delete mode 100644 node_modules/math-intrinsics/floor.js delete mode 100644 node_modules/math-intrinsics/isFinite.d.ts delete mode 100644 node_modules/math-intrinsics/isFinite.js delete mode 100644 node_modules/math-intrinsics/isInteger.d.ts delete mode 100644 node_modules/math-intrinsics/isInteger.js delete mode 100644 node_modules/math-intrinsics/isNaN.d.ts delete mode 100644 node_modules/math-intrinsics/isNaN.js delete mode 100644 node_modules/math-intrinsics/isNegativeZero.d.ts delete mode 100644 node_modules/math-intrinsics/isNegativeZero.js delete mode 100644 node_modules/math-intrinsics/max.d.ts delete mode 100644 node_modules/math-intrinsics/max.js delete mode 100644 node_modules/math-intrinsics/min.d.ts delete mode 100644 node_modules/math-intrinsics/min.js delete mode 100644 node_modules/math-intrinsics/mod.d.ts delete mode 100644 node_modules/math-intrinsics/mod.js delete mode 100644 node_modules/math-intrinsics/package.json delete mode 100644 node_modules/math-intrinsics/pow.d.ts delete mode 100644 node_modules/math-intrinsics/pow.js delete mode 100644 node_modules/math-intrinsics/round.d.ts delete mode 100644 node_modules/math-intrinsics/round.js delete mode 100644 node_modules/math-intrinsics/sign.d.ts delete mode 100644 node_modules/math-intrinsics/sign.js delete mode 100644 node_modules/math-intrinsics/test/index.js delete mode 100644 node_modules/math-intrinsics/tsconfig.json delete mode 100644 node_modules/mime-db/HISTORY.md delete mode 100644 node_modules/mime-db/LICENSE delete mode 100644 node_modules/mime-db/README.md delete mode 100644 node_modules/mime-db/db.json delete mode 100644 node_modules/mime-db/index.js delete mode 100644 node_modules/mime-db/package.json delete mode 100644 node_modules/mime-types/HISTORY.md delete mode 100644 node_modules/mime-types/LICENSE delete mode 100644 node_modules/mime-types/README.md delete mode 100644 node_modules/mime-types/index.js delete mode 100644 node_modules/mime-types/package.json delete mode 100644 node_modules/proxy-from-env/.eslintrc delete mode 100644 node_modules/proxy-from-env/.travis.yml delete mode 100644 node_modules/proxy-from-env/LICENSE delete mode 100644 node_modules/proxy-from-env/README.md delete mode 100644 node_modules/proxy-from-env/index.js delete mode 100644 node_modules/proxy-from-env/package.json delete mode 100644 node_modules/proxy-from-env/test.js delete mode 100644 package-lock.json delete mode 100644 package.json create mode 100644 php/service.php delete mode 100644 test.txt delete mode 100644 test/test.jpg diff --git a/.DS_Store b/.DS_Store index 5ba35b804a2b9bab5830f4a41daf911e69822519..b207f46e14de7348ce06ef2a7e0d9bb3b086638b 100644 GIT binary patch delta 666 zcmZn(XbG6$&uFwUK$e|j@|6o6{nw5;PCm#kGP#qT2StFtAj2>?IX}060SJ5+2W&pT z?#;m{GFgB{b+e&BA6pJHLjgkuLqSeDLKy>th8s{dR3U4BnVcud tgWxJMy`QjovE(vF7WeHIdZ54+L3SdEp)UQOiOxZNXEVFP4tmG$O#r)}zBK>< delta 1248 zcmZn(XbG6$&uF$WK$e~3ey@JF#<^pTlMk|sOzvdoK@s3D$S@2}&d)7i00PhQ2b&MD zdvh>KOcr2K-E1h($5zk7kjIeEkiwA45YLbcW|cDJ0C~kZ=?IMs+SPY~`f;kw&3AE0 z%E?axs^Q>^`nloB*<+6Q)u%w!*Au4!*%u794M4LQSQtt`&SWS-aVP_W5V9Py!#*<} z5V?2?>@c|AT)ljlOBgt?DL}E9L11E`!enj None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('orders', sa.Column('user_info_json', sa.JSON(), nullable=True)) + op.add_column('orders', sa.Column('delivery_method', sa.String(), nullable=True)) + op.add_column('orders', sa.Column('city', sa.String(), nullable=True)) + op.add_column('orders', sa.Column('delivery_address', sa.String(), nullable=True)) + op.add_column('orders', sa.Column('cdek_info', sa.JSON(), nullable=True)) + op.add_column('orders', sa.Column('courier_info', sa.JSON(), nullable=True)) + op.add_column('orders', sa.Column('items_json', sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('orders', 'items_json') + op.drop_column('orders', 'courier_info') + op.drop_column('orders', 'cdek_info') + op.drop_column('orders', 'delivery_address') + op.drop_column('orders', 'city') + op.drop_column('orders', 'delivery_method') + op.drop_column('orders', 'user_info_json') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/__pycache__/7192b0707277_add_new_order.cpython-310.pyc b/backend/alembic/versions/__pycache__/7192b0707277_add_new_order.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..141c202e8a220a9e5246c1bc78570b74681f656f GIT binary patch literal 1395 zcmaJ>OOMn>5VqSMzt;00Sq@8xG&znG@XP>f7@;WA?j<>FA~q{UzO+2<8pf>c9=dJN zaORq8&T!s=F4e3jpuQ^V?Zv(590XFayq+zyh_vHL_k`@_!MC=o=@lZZcXpr zn#bemY;o^+ac6dW7EkZS$3gH8t6)rs^3LID162m9cTr)WiKz(RoF!p5nDWFv4;6O9 zsXpV+EBb-#FGX5qYg(jBD5MG)Y@lM2@=7@Z>PA$V4%=NGDku2Ke-LWpmC4MSJh2$5 z$t(M{!7OI~WxXb5m+K+d`wHh=!MRs(-WBouE4biOjv3g{U&HB`QD-85H?55L;S8z2;2WSzd3|=z|%@L4cIX&lp^`UR0RzM&x^+D#&Xn zW$Sw42tDK~&@+_rqA4pyWU8E#ooxSl{u}0XUW>eX{KgPRc({ds%jg1=l+la)GCJj1 zQ$khC=y$;%i?ozcn*uPp;)2R1%Ro|Da{~>^N z%WdQQGjV|a#kU&1(Qr+}NW)NrPFI8o6SAxJ0TX<)FZaii?xY)dPWk$cwDKC)`Hhsgjy>Wq%Ey_$e~Njn}&_;ZkXL_TZCN1 zgAlMRipS}DkG|P#)Jk`lH@|tmH*em&_x5|`n_~z0yeZ%(e)-zg z&11Vt-X33@?)>s8!!!B|$$;A{G6Y+ENYR}@3H`{~6 z+zA-<+Wr%ISSN`R9d@b9E^xlg1^;1CZ>G)a`prFB&oP-ZbuMPPxWvUYip06I{_ytw z6?bEEWqEaLb$u<-9xZJy-&@*D3c!M{+rrXDtk?AXC^1uw=L0=pyn>k`y(ud(cbpdc zI_C`tp80Lo1j@G|kIxc$b!W&lP6tt$*n}-0Q5PAn>RR1NG}nco<+_QAkMq!h7=N%X zt7+6q{KKkjsc6YGE_5z}ow+Nc0L8yYGS)xuXxlAfJOv#@74g2>3 AEdT%j literal 3424 zcmb_f&2JmW72nxiE|<$seMGh#)!q6cv9LwivaQ%ii>MNfFjXi+q+EcP#cF4yth^u0 z>{77^8%RIUORK-2C?KMzT#BMW(H2eqgB~ap=wYtCG(i7=qKBeyW>+iHO`1!&;^WMl z_c=4a_f}!KY)J5t|MJ(P)p<$!8=p*mRG7R2-{{vs_9ZO!B#f}^Az$vvzS2_=pDUi~ z=XyCYRz0nk2VTz8{X(xGc$%mAM$dq8zE>nAQpWn*5-#*AL?I=t9?FXn@C@Loy(;Gs znUoe`cBqJzqFAYY$w~=UW^n~qf1>tgU{u31Fq*|`N2<>af4YgW8M#AZ#`b|nOgq5l zppWgCn0*@JffJiiOyOrhiDB%K6E||hz-(O1=i&vD<4oha6Yg+ z`+#^6Gcf6s07q6BFpUyOGH`e#mmhw%W%zEvH~Kr0q*yvf7{&6jg5_8~S4R?7uzIEl zP7XM^DUSAEoP5gBGpz#XYFNj{8Q49~F&ZHxrLzS-dnLb=&u2keNx9cDnYoda#&BK4w+MdterEBd8aG}l zzmV{ozf@k=Jf6qb@nW+7!qOX90uI89kU#9f2+#&QGtY1Qgwz7a%%=JCac*Kf!^c95ivWuU-j#^Z6GAOa4()*#(+b=xW+ZsyTCC{4u!LL7#b81k1TNr#=_wW?mg`d& z&{4r|dck%JX568b5|LNlAum1u$ybY-7axP>@5i5UDAb;o0c}*{n)6So>)Z77`{Z;BkPuVb4PsV< zh5}fc;9PnDl#mw;9Wukl*m4plxhGx-fK=b>?6#SHL}H7hiRQS2Jb6K!K}?tyIksm* zAWrD}Zp=!y7X!Rg6SHXshan~IQD8kMr%*6x*iQxOD*c8q7bUHYx1b2hLT?6O4`m{< zJy8r`Q$LaDLlFJfj~e?>LZZgdecsp(oq|tRy1%N5?N6U z-WvzD^Bfvo!}f@O;5rR%omYv*WD~V``r7`J)|3TQQ%Xt+)ewCP zj7;@i85jllioW{~7)dT)^QQ~48kQ`K7)5V(P0sE-ph_!#kGmCJ~5V)@o^b+DtHEZ^(>w{ zOZ2MHlTg}w=G*ip;k5=XpyJ%K(_wg++O25`Ul1rTk7R=LUzAYHly zbol2CW?uor$2#G7UR3BB5LQjmds$TII_DaKE2;>sUok`kby;AX z-sd(rgcGvC#R~l0R6vPe{)nI2E%_$8m4F86dQC2&=ogsU7url%fg{! zSxkf7fHFuqYN!H)k=u~Ps5 diff --git a/backend/alembic/versions/__pycache__/a81393a28fee_add_new_order_.cpython-310.pyc b/backend/alembic/versions/__pycache__/a81393a28fee_add_new_order_.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f9cf77fb41c529915bee2a568eda9f1e5d7b74e GIT binary patch literal 878 zcmZ`%y>8nu5T<@?$%oPHIw{T$-%4UPfV3JKH(TM_9L+=z}+RFyncyDm9mrZ(uY&sfD zhvWUjgK;(-N6~j21L?aaJcsKQhyqOGLSUi3V_dmB%j3;rCREQlOuSjnBwXGK`kPE< ziizTqmTU&4HW7y<2(GCVrm3J_DN)$6o93W%g6oI9AWqK7qjUC1RD#|g_$ks04N9l? zi>vfh7OM(Gt<#T6epal~DOaF1a5|TY>QzyIrn;62(itnRP(jK{sAh{Iy}=~dq)s=x zqc4ZPzq~WSYKfUSyt2yhIT7Dg9sK=QzS^>aV+${(Ke=rrOv?^yo%W4A*R`;X(07%6 zyJ!8b2`J^Vpwu+$yX688+uF)QFu`*V$_~mz`nqIgaS7F(aWzv78}xLoOovh?gsfR@ x8>LhQHbrQmWx=+wF8jC8wZ`zcl6+OdTYE2h2cb#2LF|T3?C&_Q@r%56^A{-{&~5+# literal 0 HcmV?d00001 diff --git a/backend/alembic/versions/__pycache__/f89a59b0e814_add_new_order_.cpython-310.pyc b/backend/alembic/versions/__pycache__/f89a59b0e814_add_new_order_.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9547a3e859a33d24c54cc5ae9b98f98982f3a170 GIT binary patch literal 1121 zcmZuwL66f$6t>5X?Kp8Zm1w1ILz3f z<=jIrTziHizk`1<*Pi$boOt6zG*ubPpWmDJ^1gZBo8&>@8@N6VgX_O6!}v!#`^!e> zH+<#a2$u#JkpU)<)XYdkGApu7?OUmx)uLKu+o=<|P>VcPXFfO&3~-|cvsfMMD{^Sy zo`-vO)YN;7u=*i-S5_6NSCQa55g#LMXhQI(9ktME!2wzaU@wjC4{z5o0Ojlr%>^() zeSg7TujM-DowMnvb2U7TPfk}oHat4sMwlFR21lp;Rd3K6^alffB3N9o&JG%34 zcYDWo`-4ueKRP}g_3z&s9t{V>Uwr>F%3ySf@|NsX$#}w4?UEx=*Hk3m%%kQ$nexQ$ z9IClbrvk{|aQZD6tVGO{YnsL@mP+LV+b|B2=3F^~6{X;brrlpHbSC)99ztQfH^D5( z9~KZq8!WJ)1`fF3K^^>eE;OL|&iX)bLIx@p%#~9~B~@z^-(-vzG-Jhe4yrL(%%*1- zbTVE{m2*CRem;Jpf`{Yz@AP6Zo-ZHG7OFiNFCWvVb2^U5w8rwMl`KGfq7Y*DOO@uDlHr)V` zZ+2b(+u_!_jd`lBFGt0$87uZS5qGvF^uKl9Z7Y{j$P-GHQ`~IUn63{gECl85n%!wE zU!`%HT(j&({G@YgI>(ZQPzRL8oaaScXh|p)u8w;e>KZhC#rxf3dJnQ3%9Q None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('orders', sa.Column('payment_method', sa.Enum('CREDIT_CARD', 'PAYPAL', 'BANK_TRANSFER', 'CASH_ON_DELIVERY', 'SBP', 'CARD', name='paymentmethod'), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('orders', 'payment_method') + # ### end Alembic commands ### diff --git a/backend/app/.DS_Store b/backend/app/.DS_Store index 55d04c02ff31eeeb8f498c6dc8ed36f8fda68bbb..8e8e3571f1105572f59de51a6fa10b967daf3e30 100644 GIT binary patch delta 128 zcmZoMXffDe!ooC}VKNVk)Z_pb9=5BG%lMkEO`gCaGr5lC1C#mx$px$$YzJrZF0HyY zc^<14jCqb#hjGH>->iyE9^RAX*s__J7$$eKZDM9-UpU!;U4bJnm|L~>+A&8UM{IH( YI~N;7U~(V33gd!_g&~{SIsWnk079B8)Bpeg delta 121 zcmZoMXffDe!ooDkW-M&lI{F_ygi8Eoc99uRM>Pjj0mm#X`~Uy| diff --git a/backend/app/__pycache__/config.cpython-310.pyc b/backend/app/__pycache__/config.cpython-310.pyc index 0200aea431530eafaa9c15b40c6a3c6779be7c71..6f3e7f452ea9e8218b90da5715ef3c5cc3b9f9bd 100644 GIT binary patch delta 79 zcmbOveoc%wpO=@50SHo7@MJ9B$g9UBQl6Sr5XD@OmmbBERgjt<#hjj*w%MO4oRwcV eqokz3N?+eV4-6-7XFtL!q@P$&uz4594JH6<@)uD6 delta 125 zcmca6Hc6Z}pO=@50SM-t`IEkMBd;ElNLE2=dK7a(9*{`SOp9VEPfaS=?9UX=I@yU$ zX!17p!}dPzq8+7hN1O-T zGq6o==}i{OH^@uXKVxVww?w#AuIp=m~?Y3XR$Ni{%@~=*% z@*_FxpE{hI_~k#qh>Eb{N@6i9sj_NPW3|L)c2Z~cq`?}A!<@urZsIX7@tJQ?el>0; zE!Hx)7PpfQ>lkduUDn0(dfZDk*oIZPRS^y0d{+^U?91+b@4{xAh`GW;%)5$hA?AxF zVokYmQDfKOw?rF$+xXYvcSIL{*Z409dtMpze!}c417A1BS-ebA?d+$8yeE}z%nKEz z_jG+P&Um4noh*wbPqn|7DH+{MKbL3P-Q|Tmj1sBsL-}o??Mb>!wEg$_o$0{REhS@K zL|K}jL<{)#@iGnRfxW|X8PdS(9}CV+{QmPcjI1zAR9ID5tR||=7ByBEHfxAFb3}u= z!olQS-MpiORP%xt%UnAPnF>sq`3seWGS3M+H26Xa<_Q@`UrQy0_ClV9GL8{-mBg%s z6w`sH-5^MLB7;EtL6Br(853>=!B3K`#o~;4e0TUoE>%7}k4}fXS-4DOTI9o- z%I+ziQ^YnlYU1#hL115RbMJ z#I3j$+q(R^mU;HVy;of{z`nL;{EX)F4_TaKf;B^ITT}!gSAsPb{A|Hv);{9tX;3Jh z=EqX8UdZ#4AWMVQKA-7YezedwwZZ19P4uj`$XtVA%`rA<|1cPj4t7~%Ho7|--Dd5b z(e(4+@L)8Z?@bO^Z#atB8YB$oPrGg)B$NW4??FYEVG8=UFO& zJLm|~z^l7@nx)HyfV#C!ETtUcU9iX&ONd(Lx&x-tFkhCVOb44!y<*)Cf`VpLDo0i6 zoz!4-H4@4p;Lr+!NN9gGSHdpTmKITQrd(>#ro>vBeSm`&Qp-S=aGx)moOKj2AQJ18m~YWQ-^M3rqU5uPT`;DBcn|T774EO$%{9CQe3^f34POT)rn=xI^;TmqA>zrGSALbf0{<7Q-d|m7 z`5N%$sIRZ(yaD`6zTS-oaL@8!&rnK=3`7^9{oXF3Q?R#8pFj_(w8-BJ)0(4`6hY zcsVLlwP$)a!l|1P&ig!$-+dFcNEY(FOf61$I;g4-silKT6>=e`gBwp#R{fc}zapyo z3%SHp^;a@(f{tMF>|IHZI;4I;GGtuR}vh?GL!ra94YwgszHh{jYt-9crhs=ex3(Gj4# zmq8&59>ux#O;IsZ(p{AA@Ra((!F>b))zb{(exf|}Df&=m;Y79so-CVwJhnK zhuu|4??E!SRhi=52d3h^Sr%`#hpFhMq2H=-8#C#|U8_~ys%?2a Juh-qW@n7Ju`5XWM delta 1407 zcma)5&yU+g6!!Q>Y_Ai?NxVPO?rwLf5Jc?;*iy=Fp#?SB3RnfH{5TAV$Ye%cgB_=1 z2dSk}kd_NndsxlA>fY#$Lr?qzoO|iPH+n-tf-@2l?|FCINbG@F{_?%=J-->hZ~Vi; zuXAovsmv(w`}y&cn|EjS-9_?f|J>SyC`@5$G!r}2DYzDu;|i@5T#sDpLf(j~u}3|k zTvu3$ncpkSQpB?myQ~VaUoC_= zrhlZgyx(zyEqnjZ59B*!lce%n@=KfAo$mH~pLTC_JGp)B-QKnC_I9^JtsC8~PkNnh z=b<9z;jse#{!9+ktE4Rt)WsDSR2%LTiKXb|__ByVNd}~;ri=0}S;Eyz@*lNZ?X^r% zgQTb^4e1l--+ zC3G1`m3gej>ac|~tO4`%bK^t7xS*v3|I&}eY1q412G|m3ke-#d%=zYvNMAx|A)G@v z4?yLCd2zA^EH?*1ItUo=wN|Fh!OB+<5eqo~HefftMi^x})HB%!gS&=Gjid1fD5cJr)_yf-p*j zUZ|M}247>B;W+*h-rX1`8Bc`;Nl`|49pNIv8whv^#3cYKSMBEYX9s!eplv|*Ww_H7 z06b`}S|@cOSa|Khb1GvEK7Xa9(-8#u*~e7?-sHLn|!}}zH`p^ zJLmh|)BE?k?D{Y(h~?+oB>cr1{@s6MXsRGu?y%yPe9gGxix`|PU*4x0RO1OLCAS;5 zNrSA{pqf;r!_=wt8kv-m#?2|?1RJ-cjP2HOTgouOaA%*8dQB;lYC34-ffwhr=c#7Z za)vPp`K-v7->c;K6v3zD`1!6_2glpyp%kF zlxR67BqqHjsd=rEQA!jZeUC}K7AEx+VyuW_IhOoHamq5tE=psv&ZA=9F-te=mb?Wm zf4L?2v)t1cVkpS`Pe#Lls})zg7-2sa+-vG(Db_BH%7^4uiI=0>%lJBa$aAvKKW9GN z#v^Ufgb3;2!RVZ=u_F>sgkqgt0Ru9}iM?3rDO1B8;Y2vt#bau+C6w?rcEmz$9q|Af z^ZA^*^0GNga~n^*nh1Ap$Bi?7G#Wa zW6v?wB2`?J*;5!o*@6+2BpwXKlK5(fb5D`p(Et5?z>1u3+2i!Z8!^Skj7%0wd0wdG z8J02&bIbaZ{5GaMmrXX=0{wwS$}sgX1kdVy@QUAQ^~!o{Lr_)Wv9H9}&8sP61Tz*v zcg0zkXMPM;%lrchokYm2^8One#rE$PB$*VVRn=^?|FGb@3f(oouoKX-zDBu@eBwr>pxKHb zy?$#Mx%BxdAWIC9lI&935@G%sc|M2ZFE0guZ9cmOf!b>J6CA2-W;Zg^wJVsJPH#`X zp3`VCSn9kCn{YT+ynt7i;R~H~6lF-&<=L;$_8P&L;zHN!T*WLTVbGG@P`rTi8!Q=Z4>x&}@iSyav9EY{9h!EhuPOg6bAR zeN&O8fTu+eZf@M+x`XCie)tG}jUv(g!8m?z_ywAWe0zw8QE=g^0bgSSo6a0)v@?Ay z2Z(~M%`*gIEE{r?yWs0hP8>*-ToEm#;pwJ&JC)K$@2*}zf1{$nPG|}2Q0`$yNQO;i zrH~2SW24Z%`8UB+3Tjh;U4Y4^dX|7EO$}@&vv|u@wp;uyM~GPb$HvH_@#W(LlLS`@ jZV=GRjzo#(u17%^ z03=NZ9ouy;B zUiObOE9ecBC&}`@=!e36Md}3ZK8~nHOhPatPec1zEG1J3Q)7@`Oo;&oi!YGb=x%X~ zLJVMj9B~VA6A^^1vTW#7XTqK}SLw)=GTBSuD6cJA0`S>lQu1Dt5Xd(brpUENP&tN} zK@1?K5PbIA7)`^Ad;@fq=t&a{mW(INu_Q!nSqbzy3&yQMBDX=uc)1yFM`x*C2%#Or zSY#eHOHY$Kd_}aS3tPCtjS4ZNilEiz6t-iz1z54YCxg&mRzjB8SUsjCEOHE?K@>6U zDE&R>+xon888dg`YF;+9*wthay6hJ643_O45{`bi=a3{+*ZCXSDeJjGWe$mi(3_n9 z)RjlSCl{Syqqo`v39?qF&Wai7oe&LNUd@YCT=SYb)DBn2&rlM)-~188zK%RTs2 z;#yg@?-YeUF=JN2SkbNpCey{U%G?rwK!q!P4Ljk*kQDM+t?{nED$)dfZ0kA*l|zCt z_Phb1%5UjGl)6wFPY+?d1{1DY<+>bxbk&OwS-7M2UiS6&`MNt=dVN>Cbc!EzqgzFC zpxIqcHls6cfgI#_?HWq4B7KCw2LAGYev0tpq>kM%=BJXs^g4)Db^%++cj+-N+*73@ zPUx?yS3Y4Y=M+$BGQiiWrz8M#)nB;Y0N`gQR sECTN#okxTa3y3fRPlK)^&{oQ)-oR)Rfz4?IQNaB?RkuwpwSH77o$G`C=LE!R3O##@th ZM9qO@is&@P`pv&ZRTvrTCo72W0s!wA7GnSa delta 74 zcmaE$e@mY?pO=@50SL~mx|VLTkynqI@z-V>W*c@^mXyrolFiLr>$n)BCL4*G1IaAW bX^gd-8O2l>*=m7WiliogWD%ZhC%y;(Iaw8U diff --git a/backend/app/repositories/__pycache__/order_repo.cpython-310.pyc b/backend/app/repositories/__pycache__/order_repo.cpython-310.pyc index e475609b95940f18695277a19dd222b0f0f07523..b77d46a3ba3e1d0a0f98cc026c0b0731a403a840 100644 GIT binary patch delta 7334 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 diff --git a/backend/app/repositories/__pycache__/user_repo.cpython-310.pyc b/backend/app/repositories/__pycache__/user_repo.cpython-310.pyc index 829620962d19d46cf32bc8fbe0e427c0a887de0f..ff7d1a8a094868cdb2e09a605696dd7960fac5c8 100644 GIT binary patch literal 7958 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 delta 2613 zcmb7GOKV$46xK*rS66Szww~52j$f)BCvoD|EqOX|^G=!vb#fD;%AS!UtQT{y(*_FK zgf0Rpqyr6IG|*)>B@jv%+4KiA>%xU@@&l5VQs}DmoVk*GQA42yf0{Yp%sJmVb7rKk zkGxupw8P~|eiX^M|00j(m%GG8UDVqMG=qAu8PY>?t~SEWh#qN1^=LDu z$C`0HF73X?NOM#lZI0<WibC>^`+)iaV#un24og2NLHBMO zj2k?Qq=a!8S@BI~R*a}@wcfzsh5`_AXBA!&zbhB7%V1tdFtuH)8O-Fn(OVIq zA8G;((mE8#-w$MDa7i9v>iJ`Dk&uG;Q9UNMf>{yw&8a113Sz}KIT^v>90F#@=MnZG zEFkPf*e4#q9$Bzy@xoWg4TTMEa8@;$OffJ1^c^M3&zH1iLS{wFpUk6kJ=Uoj#;0vg zD;w<=oI%sB;Eedrf1c#TYyWH)l~|rmyJJ%&480$1-nz+}hUJ4|R85of1IQdi@E|M! zlzlq~%BRu#9)K0x?od<)P_;Z;?4ISlTWxH!;S<7j{(cHa#FM}Ya#Fksbk+CK)vVYI zp6TXrh-b#V2!{YHm9hqd)6%vxjPm6_-^4cgmcQ&UV4e}DCa3n85?>?PD!%(y!Hf5Dk&J5!V^@dSe73}9+SdY7x0#WSI$&yXA9O8T*S z9hc9FH|fLwPyQ77rreYr?|-mWJCl+ML;AO)4TOI>m^b(Bt6v8sXNdQY( zZ?_v bool: detail="Продукт не найден" ) + # Получаем ID всех вариантов продукта + variant_ids = [variant.id for variant in db_product.variants] + + # Проверяем, используются ли варианты в корзинах + from app.models.order_models import CartItem + cart_items_count = db.query(CartItem).filter(CartItem.variant_id.in_(variant_ids)).count() + if cart_items_count > 0: + # Удаляем все элементы корзины, связанные с вариантами продукта + db.query(CartItem).filter(CartItem.variant_id.in_(variant_ids)).delete(synchronize_session=False) + + # Проверяем, используются ли варианты в заказах + from app.models.order_models import OrderItem + order_items_count = db.query(OrderItem).filter(OrderItem.variant_id.in_(variant_ids)).count() + if order_items_count > 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Невозможно удалить продукт, так как его варианты используются в {order_items_count} заказах" + ) + try: + # Удаляем продукт (варианты и изображения удалятся автоматически благодаря cascade) db.delete(db_product) db.commit() return True - except Exception: + except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Ошибка при удалении продукта" + detail=f"Ошибка при удалении продукта: {str(e)}" ) diff --git a/backend/app/repositories/content_repo.py b/backend/app/repositories/content_repo.py index 7465731..22ba624 100644 --- a/backend/app/repositories/content_repo.py +++ b/backend/app/repositories/content_repo.py @@ -21,7 +21,7 @@ def generate_slug(title: str) -> str: slug = re.sub(r'-+', '-', slug) # Удаляем дефисы в начале и конце slug = slug.strip('-') - + return slug @@ -35,16 +35,16 @@ def get_page_by_slug(db: Session, slug: str) -> Optional[Page]: def get_pages( - db: Session, - skip: int = 0, - limit: int = 100, + db: Session, + skip: int = 0, + limit: int = 100, published_only: bool = True ) -> List[Page]: query = db.query(Page) - + if published_only: query = query.filter(Page.is_published == True) - + return query.order_by(Page.title).offset(skip).limit(limit).all() @@ -52,14 +52,14 @@ def create_page(db: Session, page: PageCreate) -> Page: # Если slug не предоставлен, генерируем его из заголовка if not page.slug: page.slug = generate_slug(page.title) - + # Проверяем, что страница с таким slug не существует if get_page_by_slug(db, page.slug): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Страница с таким slug уже существует" ) - + # Создаем новую страницу db_page = Page( title=page.title, @@ -69,7 +69,7 @@ def create_page(db: Session, page: PageCreate) -> Page: meta_description=page.meta_description, is_published=page.is_published ) - + try: db.add(db_page) db.commit() @@ -90,10 +90,10 @@ def update_page(db: Session, page_id: int, page: PageUpdate) -> Page: status_code=status.HTTP_404_NOT_FOUND, detail="Страница не найдена" ) - + # Обновляем только предоставленные поля - update_data = page.dict(exclude_unset=True) - + update_data = page.model_dump(exclude_unset=True) + # Если slug изменяется, проверяем его уникальность if "slug" in update_data and update_data["slug"] != db_page.slug: if get_page_by_slug(db, update_data["slug"]): @@ -101,7 +101,7 @@ def update_page(db: Session, page_id: int, page: PageUpdate) -> Page: status_code=status.HTTP_400_BAD_REQUEST, detail="Страница с таким slug уже существует" ) - + # Если заголовок изменяется и slug не предоставлен, генерируем новый slug if "title" in update_data and "slug" not in update_data: update_data["slug"] = generate_slug(update_data["title"]) @@ -111,11 +111,11 @@ def update_page(db: Session, page_id: int, page: PageUpdate) -> Page: status_code=status.HTTP_400_BAD_REQUEST, detail="Страница с таким slug уже существует" ) - + # Применяем обновления for key, value in update_data.items(): setattr(db_page, key, value) - + try: db.commit() db.refresh(db_page) @@ -135,7 +135,7 @@ def delete_page(db: Session, page_id: int) -> bool: status_code=status.HTTP_404_NOT_FOUND, detail="Страница не найдена" ) - + try: db.delete(db_page) db.commit() @@ -152,7 +152,7 @@ def delete_page(db: Session, page_id: int) -> bool: def log_analytics_event(db: Session, log: AnalyticsLogCreate) -> AnalyticsLog: # Создаем новую запись аналитики db_log = AnalyticsLog( - user_id=log.user_id, + user_id=log.user_id, # Может быть None для неавторизованных пользователей event_type=log.event_type, page_url=log.page_url, product_id=log.product_id, @@ -162,7 +162,7 @@ def log_analytics_event(db: Session, log: AnalyticsLogCreate) -> AnalyticsLog: referrer=log.referrer, additional_data=log.additional_data ) - + try: db.add(db_log) db.commit() @@ -177,9 +177,9 @@ def log_analytics_event(db: Session, log: AnalyticsLogCreate) -> AnalyticsLog: def get_analytics_logs( - db: Session, - skip: int = 0, - limit: int = 100, + db: Session, + skip: int = 0, + limit: int = 100, event_type: Optional[str] = None, user_id: Optional[int] = None, product_id: Optional[int] = None, @@ -188,31 +188,31 @@ def get_analytics_logs( end_date: Optional[datetime] = None ) -> List[AnalyticsLog]: query = db.query(AnalyticsLog) - + # Применяем фильтры if event_type: query = query.filter(AnalyticsLog.event_type == event_type) - + if user_id: query = query.filter(AnalyticsLog.user_id == user_id) - + if product_id: query = query.filter(AnalyticsLog.product_id == product_id) - + if category_id: query = query.filter(AnalyticsLog.category_id == category_id) - + if start_date: query = query.filter(AnalyticsLog.created_at >= start_date) - + if end_date: query = query.filter(AnalyticsLog.created_at <= end_date) - + return query.order_by(AnalyticsLog.created_at.desc()).offset(skip).limit(limit).all() def get_analytics_report( - db: Session, + db: Session, period: str = "day", start_date: Optional[datetime] = None, end_date: Optional[datetime] = None @@ -229,44 +229,44 @@ def get_analytics_report( start_date = datetime.utcnow() - timedelta(days=365) else: start_date = datetime.utcnow() - timedelta(days=30) # По умолчанию 30 дней - + if not end_date: end_date = datetime.utcnow() - + # Получаем все события за указанный период logs = db.query(AnalyticsLog).filter( AnalyticsLog.created_at >= start_date, AnalyticsLog.created_at <= end_date ).all() - + # Подсчитываем статистику total_visits = len(logs) unique_visitors = len(set([log.ip_address for log in logs if log.ip_address])) - + # Подсчитываем просмотры страниц page_views = {} for log in logs: if log.event_type == "page_view" and log.page_url: page_views[log.page_url] = page_views.get(log.page_url, 0) + 1 - + # Подсчитываем просмотры продуктов product_views = {} for log in logs: if log.event_type == "product_view" and log.product_id: product_id = str(log.product_id) product_views[product_id] = product_views.get(product_id, 0) + 1 - + # Подсчитываем добавления в корзину cart_additions = sum(1 for log in logs if log.event_type == "add_to_cart") - + # Подсчитываем заказы и выручку orders_count = sum(1 for log in logs if log.event_type == "order_created") - + # Для расчета выручки и среднего чека нам нужны данные о заказах # В данном примере мы просто используем заглушки revenue = 0 average_order_value = 0 - + # Формируем отчет report = { "period": period, @@ -281,5 +281,5 @@ def get_analytics_report( "revenue": revenue, "average_order_value": average_order_value } - - return report \ No newline at end of file + + return report \ No newline at end of file diff --git a/backend/app/repositories/order_repo.py b/backend/app/repositories/order_repo.py index 90cb5bb..9d03778 100644 --- a/backend/app/repositories/order_repo.py +++ b/backend/app/repositories/order_repo.py @@ -1,13 +1,15 @@ from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from fastapi import HTTPException, status -from typing import List, Optional, Dict, Any +from typing import List, Optional, Dict, Any, Union, Tuple from datetime import datetime +import json from app.models.order_models import CartItem, Order, OrderItem, OrderStatus, PaymentMethod from app.models.catalog_models import Product, ProductImage, ProductVariant, Size from app.models.user_models import User, UserAddress -from app.schemas.order_schemas import CartItemCreate, CartItemUpdate, OrderCreate, OrderUpdate +from app.schemas.order_schemas import CartItemCreate, CartItemUpdate, OrderCreate, OrderUpdate, OrderCreateNew +from app.repositories import user_repo # Функции для работы с корзиной @@ -34,7 +36,7 @@ def create_cart_item(db: Session, cart_item: CartItemCreate, user_id: int) -> Ca status_code=status.HTTP_404_NOT_FOUND, detail="Вариант продукта не найден" ) - + # Проверяем, что продукт активен product = db.query(Product).filter(Product.id == variant.product_id).first() if not product or not product.is_active: @@ -42,7 +44,7 @@ def create_cart_item(db: Session, cart_item: CartItemCreate, user_id: int) -> Ca status_code=status.HTTP_400_BAD_REQUEST, detail="Продукт не активен или не найден" ) - + # Проверяем, есть ли уже такой товар в корзине existing_item = get_cart_item_by_variant(db, user_id, cart_item.variant_id) if existing_item: @@ -58,14 +60,14 @@ def create_cart_item(db: Session, cart_item: CartItemCreate, user_id: int) -> Ca status_code=status.HTTP_400_BAD_REQUEST, detail="Ошибка при обновлении элемента корзины" ) - + # Создаем новый элемент корзины db_cart_item = CartItem( user_id=user_id, variant_id=cart_item.variant_id, quantity=cart_item.quantity ) - + try: db.add(db_cart_item) db.commit() @@ -84,20 +86,20 @@ def update_cart_item(db: Session, cart_item_id: int, cart_item: CartItemUpdate, CartItem.id == cart_item_id, CartItem.user_id == user_id ).first() - + if not db_cart_item: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Элемент корзины не найден или не принадлежит пользователю" ) - + # Обновляем только предоставленные поля - update_data = cart_item.dict(exclude_unset=True) - + update_data = cart_item.model_dump(exclude_unset=True) + # Применяем обновления for key, value in update_data.items(): setattr(db_cart_item, key, value) - + try: db.commit() db.refresh(db_cart_item) @@ -115,13 +117,13 @@ def delete_cart_item(db: Session, cart_item_id: int, user_id: int) -> bool: CartItem.id == cart_item_id, CartItem.user_id == user_id ).first() - + if not db_cart_item: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Элемент корзины не найден или не принадлежит пользователю" ) - + try: db.delete(db_cart_item) db.commit() @@ -152,38 +154,211 @@ def get_order(db: Session, order_id: int) -> Optional[Order]: return db.query(Order).filter(Order.id == order_id).first() +def create_order_new(db: Session, order: OrderCreateNew, user_id: Optional[int] = None) -> Order: + """ + Создает новый заказ на основе новой структуры данных. + Если пользователь не авторизован (user_id=None), создает нового пользователя. + + Args: + db: Сессия базы данных + order: Данные для создания заказа + user_id: ID пользователя (None, если пользователь не авторизован) + + Returns: + Созданный заказ + + Raises: + HTTPException: Если произошла ошибка при создании заказа + """ + # Подготовим информацию о пользователе для сохранения в JSON + user_info_dict = { + "first_name": order.user_info.first_name, + "last_name": order.user_info.last_name, + "email": order.user_info.email, + "phone": order.user_info.phone + } + + # Если пользователь не авторизован, создаем нового пользователя или находим существующего + user_created_message = None + if user_id is None: + try: + user, is_new, message = user_repo.find_or_create_user_from_order(db, user_info_dict) + user_id = user.id + user_created_message = message + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Ошибка при создании пользователя: {str(e)}" + ) + + # Подготовим информацию о товарах для сохранения в JSON + items_json = [] + for item in order.items: + # Получаем информацию о варианте и продукте + variant = db.query(ProductVariant).filter(ProductVariant.id == item.variant_id).first() + if not variant: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Вариант товара с ID {item.variant_id} не найден" + ) + + product = db.query(Product).filter(Product.id == variant.product_id).first() + if not product: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Продукт для варианта с ID {item.variant_id} не найден" + ) + + # Получаем размер для варианта + size = db.query(Size).filter(Size.id == variant.size_id).first() + size_name = size.name if size else "Unknown" + + # Получаем основное изображение продукта + image = db.query(ProductImage).filter( + ProductImage.product_id == product.id, + ProductImage.is_primary == True + ).first() + + if not image: + # Если нет основного изображения, берем первое доступное + image = db.query(ProductImage).filter( + ProductImage.product_id == product.id + ).first() + + image_url = image.image_url if image else None + + # Добавляем информацию о товаре в JSON + items_json.append({ + "product_id": product.id, + "variant_id": variant.id, + "quantity": item.quantity, + "price": item.price, + "product_name": product.name, + "variant_name": size_name, + "product_image": image_url, + "slug": product.slug + }) + + # Преобразуем строковое значение payment_method в значение перечисления PaymentMethod + payment_method_str = order.payment_method + + # Прямое сопоставление с известными значениями + if payment_method_str == "sbp": + payment_method = PaymentMethod.SBP + elif payment_method_str == "card": + payment_method = PaymentMethod.CARD + else: + # Если не нашли соответствия, используем значение по умолчанию + payment_method = PaymentMethod.CARD + # Логируем ошибку + print(f"Неизвестный метод оплаты: {order.payment_method}. Используем значение по умолчанию: {payment_method}") + + print(f"Метод оплаты: {payment_method} {type(payment_method)}") + print(f"Тип payment_method_str: {type(payment_method_str)}") + print(f"payment_method_str: {payment_method.value}") + payment_method_str = payment_method.value + print(f"payment_method_str: {payment_method_str} {type(payment_method_str)}") + payment_method = payment_method_str + + # Создаем новый заказ + new_order = Order( + user_id=user_id, + status=OrderStatus.PENDING, + + # JSON с информацией о пользователе + user_info_json=user_info_dict, + + # Информация о доставке + delivery_method=order.delivery.method, + city=order.delivery.address.city, + delivery_address=order.delivery.address.formatted_address or f"{order.delivery.address.city}, {order.delivery.address.street} {order.delivery.address.house}", + + # Информация о CDEK + cdek_info=order.delivery.cdek_info.model_dump() if order.delivery.cdek_info else None, + + # JSON с информацией о товарах + items_json=items_json, + + # Информация об оплате + payment_method=payment_method, + notes=order.comment, + + # Общая сумма заказа (будет обновлена после добавления товаров) + total_amount=0 + ) + + db.add(new_order) + db.flush() # Получаем ID заказа + + # Добавляем товары в заказ через разводную таблицу + for item in order.items: + # Создаем элемент заказа + order_item = OrderItem( + order_id=new_order.id, + variant_id=item.variant_id, + quantity=item.quantity, + price=item.price + ) + db.add(order_item) + new_order.total_amount += item.price * item.quantity + + # Проверяем, были ли добавлены элементы заказа + if new_order.total_amount == 0: + db.rollback() + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Не удалось создать заказ: корзина пуста или товары недоступны" + ) + + try: + db.commit() + db.refresh(new_order) + + # Добавляем информацию о создании пользователя в заказ + if user_created_message: + new_order.user_created_message = user_created_message + + return new_order + except Exception as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Ошибка при создании заказа: {str(e)}" + ) + + def get_user_orders(db: Session, user_id: int, skip: int = 0, limit: int = 100) -> List[Order]: return db.query(Order).filter(Order.user_id == user_id).order_by(Order.created_at.desc()).offset(skip).limit(limit).all() def get_all_orders( - db: Session, - skip: int = 0, - limit: int = 100, + db: Session, + skip: int = 0, + limit: int = 100, status: Optional[OrderStatus] = None ) -> List[Order]: query = db.query(Order) - + if status: query = query.filter(Order.status == status) - + return query.order_by(Order.created_at.desc()).offset(skip).limit(limit).all() -def create_order(db: Session, order: OrderCreate, user_id: int) -> Order: +def create_order(db: Session, order: OrderCreate, user_id: Optional[int] = None) -> Order: # Проверяем, что адрес доставки существует и принадлежит пользователю, если указан if order.shipping_address_id: address = db.query(UserAddress).filter( UserAddress.id == order.shipping_address_id, UserAddress.user_id == user_id ).first() - + if not address: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Указанный адрес доставки не найден" ) - + # Создаем новый заказ new_order = Order( user_id=user_id, @@ -193,13 +368,13 @@ def create_order(db: Session, order: OrderCreate, user_id: int) -> Order: notes=order.notes, total_amount=0 # Будет обновлено после добавления товаров ) - + db.add(new_order) db.flush() # Получаем ID заказа - + # Получаем элементы корзины пользователя cart_items = [] - + # Если указаны конкретные элементы корзины if order.cart_items: cart_items = db.query(CartItem).filter( @@ -218,7 +393,7 @@ def create_order(db: Session, order: OrderCreate, user_id: int) -> Order: status_code=status.HTTP_404_NOT_FOUND, detail=f"Вариант товара с ID {item_data.variant_id} не найден" ) - + # Получаем продукт для варианта product = db.query(Product).filter(Product.id == variant.product_id).first() if not product: @@ -227,10 +402,10 @@ def create_order(db: Session, order: OrderCreate, user_id: int) -> Order: status_code=status.HTTP_404_NOT_FOUND, detail=f"Продукт для варианта с ID {item_data.variant_id} не найден" ) - + # Определяем цену для товара (используем discount_price если есть, иначе price) price = product.discount_price if product.discount_price and product.discount_price > 0 else product.price - + # Создаем элемент заказа order_item = OrderItem( order_id=new_order.id, @@ -243,7 +418,7 @@ def create_order(db: Session, order: OrderCreate, user_id: int) -> Order: # Иначе используем все элементы корзины пользователя else: cart_items = db.query(CartItem).filter(CartItem.user_id == user_id).all() - + # Если используем элементы корзины if cart_items: # Создаем элементы заказа из элементов корзины @@ -252,15 +427,15 @@ def create_order(db: Session, order: OrderCreate, user_id: int) -> Order: variant = db.query(ProductVariant).filter(ProductVariant.id == cart_item.variant_id).first() if not variant: continue - + # Получаем продукт product = db.query(Product).filter(Product.id == variant.product_id).first() if not product: continue - + # Определяем цену для товара (используем discount_price если есть, иначе price) price = product.discount_price if product.discount_price and product.discount_price > 0 else product.price - + # Создаем элемент заказа order_item = OrderItem( order_id=new_order.id, @@ -270,7 +445,7 @@ def create_order(db: Session, order: OrderCreate, user_id: int) -> Order: ) db.add(order_item) new_order.total_amount += price * cart_item.quantity - + # Очищаем корзину пользователя после создания заказа if cart_items and not order.cart_items: # Если используем все элементы корзины, очищаем всю корзину @@ -281,7 +456,7 @@ def create_order(db: Session, order: OrderCreate, user_id: int) -> Order: CartItem.id.in_([item.id for item in cart_items]), CartItem.user_id == user_id ).delete(synchronize_session=False) - + # Проверяем, были ли добавлены элементы заказа if new_order.total_amount == 0: db.rollback() @@ -289,7 +464,7 @@ def create_order(db: Session, order: OrderCreate, user_id: int) -> Order: status_code=status.HTTP_400_BAD_REQUEST, detail="Не удалось создать заказ: корзина пуста или товары недоступны" ) - + try: db.commit() db.refresh(new_order) @@ -305,16 +480,16 @@ def create_order(db: Session, order: OrderCreate, user_id: int) -> Order: def update_order(db: Session, order_id: int, order_update: OrderUpdate, is_admin: bool = False) -> Order: """ Обновляет информацию о заказе. - + Args: db: Сессия базы данных order_id: ID заказа order_update: Данные для обновления is_admin: Флаг, указывающий, является ли пользователь администратором - + Returns: Обновленный заказ - + Raises: HTTPException: Если заказ не найден или нет прав на обновление """ @@ -325,7 +500,7 @@ def update_order(db: Session, order_id: int, order_update: OrderUpdate, is_admin status_code=status.HTTP_404_NOT_FOUND, detail="Заказ не найден" ) - + # Проверяем возможность обновления статуса if order_update.status: # Если заказ уже отменен или доставлен, нельзя менять его статус @@ -334,14 +509,14 @@ def update_order(db: Session, order_id: int, order_update: OrderUpdate, is_admin status_code=status.HTTP_400_BAD_REQUEST, detail=f"Невозможно изменить статус заказа из {order.status}" ) - + # Если пользователь не админ, он может только отменить заказ if not is_admin and order_update.status != OrderStatus.CANCELLED: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Пользователи могут только отменить заказ" ) - + # Если пользователь хочет отменить заказ, проверяем возможность отмены if order_update.status == OrderStatus.CANCELLED: if order.status not in [OrderStatus.PENDING, OrderStatus.PROCESSING]: @@ -349,7 +524,7 @@ def update_order(db: Session, order_id: int, order_update: OrderUpdate, is_admin status_code=status.HTTP_400_BAD_REQUEST, detail="Нельзя отменить заказ, который уже отправлен или доставлен" ) - + # Проверяем другие поля для обновления if not is_admin: # Обычные пользователи могут обновлять только статус (отмена) @@ -359,10 +534,10 @@ def update_order(db: Session, order_id: int, order_update: OrderUpdate, is_admin status_code=status.HTTP_403_FORBIDDEN, detail=f"Пользователи не могут изменять поле {field}" ) - + # Обновляем поля заказа - update_data = order_update.dict(exclude_unset=True) - + update_data = order_update.model_dump(exclude_unset=True) + # Если указан адрес доставки, проверяем его существование if "shipping_address_id" in update_data: address = db.query(UserAddress).filter(UserAddress.id == update_data["shipping_address_id"]).first() @@ -371,11 +546,11 @@ def update_order(db: Session, order_id: int, order_update: OrderUpdate, is_admin status_code=status.HTTP_404_NOT_FOUND, detail="Указанный адрес доставки не найден" ) - + # Применяем обновления for key, value in update_data.items(): setattr(order, key, value) - + try: db.commit() db.refresh(order) @@ -395,14 +570,14 @@ def delete_order(db: Session, order_id: int, is_admin: bool = False) -> bool: status_code=status.HTTP_404_NOT_FOUND, detail="Заказ не найден" ) - + # Только администраторы могут удалять заказы if not is_admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Недостаточно прав для удаления заказа" ) - + try: db.delete(db_order) db.commit() @@ -423,29 +598,29 @@ def get_cart_with_product_details(db: Session, user_id: int) -> List[Dict[str, A # Получаем элементы корзины пользователя cart_items = db.query(CartItem).filter(CartItem.user_id == user_id).all() result = [] - + for cart_item in cart_items: # Получаем вариант продукта variant = db.query(ProductVariant).filter(ProductVariant.id == cart_item.variant_id).first() if not variant: continue - + # Получаем продукт product = db.query(Product).filter(Product.id == variant.product_id).first() if not product: continue - + # Получаем размер варианта size = db.query(Size).filter(Size.id == variant.size_id).first() if variant.size_id else None size_name = size.name if size else '' - + # Получаем основное изображение продукта product_image = None primary_image = db.query(ProductImage).filter( ProductImage.product_id == product.id, ProductImage.is_primary == True ).first() - + if primary_image: product_image = primary_image.image_url else: @@ -453,10 +628,10 @@ def get_cart_with_product_details(db: Session, user_id: int) -> List[Dict[str, A any_image = db.query(ProductImage).filter(ProductImage.product_id == product.id).first() if any_image: product_image = any_image.image_url - + # Определяем цену товара (используем discount_price если есть, иначе price) price = product.discount_price if product.discount_price and product.discount_price > 0 else product.price - + # Формируем элемент корзины с деталями cart_item_details = { "id": cart_item.id, @@ -473,9 +648,9 @@ def get_cart_with_product_details(db: Session, user_id: int) -> List[Dict[str, A "variant_name": size_name, "total_price": price * cart_item.quantity } - + result.append(cart_item_details) - + return result @@ -487,12 +662,12 @@ def get_order_with_details(db: Session, order_id: int) -> Optional[Dict]: order = db.query(Order).filter(Order.id == order_id).first() if not order: return None - + # Получаем информацию о пользователе user = db.query(User).filter(User.id == order.user_id).first() user_email = user.email if user else None user_name = f"{user.first_name} {user.last_name}" if user and user.first_name and user.last_name else None - + # Получаем адрес доставки shipping_address = None if order.shipping_address_id: @@ -508,41 +683,41 @@ def get_order_with_details(db: Session, order_id: int) -> Optional[Dict]: "country": address.country, "is_default": address.is_default } - + # Получаем элементы заказа с информацией о продуктах items = [] order_items = db.query(OrderItem).filter(OrderItem.order_id == order.id).all() - + for item in order_items: # Получаем вариант продукта variant = db.query(ProductVariant).filter(ProductVariant.id == item.variant_id).first() variant_name = None size_name = None - + if variant: # Получаем размер варианта size = db.query(Size).filter(Size.id == variant.size_id).first() if variant.size_id else None if size: size_name = size.name variant_name = f"{size.name}" - + # Получаем информацию о продукте product = None product_name = "Удаленный продукт" product_image = None - + if variant: product = db.query(Product).filter(Product.id == variant.product_id).first() - + if product: product_name = product.name - + # Получаем основное изображение продукта primary_image = db.query(ProductImage).filter( ProductImage.product_id == product.id, ProductImage.is_primary == True ).first() - + if primary_image: product_image = primary_image.image_url else: @@ -550,10 +725,10 @@ def get_order_with_details(db: Session, order_id: int) -> Optional[Dict]: any_image = db.query(ProductImage).filter( ProductImage.product_id == product.id ).first() - + if any_image: product_image = any_image.image_url - + # Добавляем информацию об элементе заказа items.append({ "id": item.id, @@ -568,17 +743,43 @@ def get_order_with_details(db: Session, order_id: int) -> Optional[Dict]: "size": size_name, "total_price": item.price * item.quantity }) - + + # Получаем информацию о пользователе из JSON + user_info = order.user_info_json or {} + # Формируем результат - return { + result = { "id": order.id, "user_id": order.user_id, - "user_email": user_email, - "user_name": user_name, "status": order.status, "total_amount": order.total_amount, + + # Информация о пользователе из JSON + "user_info_json": order.user_info_json, + + # Извлекаем информацию о пользователе для отображения + "first_name": user_info.get("first_name", ""), + "last_name": user_info.get("last_name", ""), + "email": user_info.get("email", user_email), + "phone": user_info.get("phone", ""), + "user_email": user_email, # Для обратной совместимости + "user_name": user_name, # Для обратной совместимости + + # Информация о доставке + "delivery_method": order.delivery_method, + "city": order.city, + "delivery_address": order.delivery_address, + "cdek_info": order.cdek_info, + "courier_info": order.courier_info, + + # Информация о товарах из JSON + "items_json": order.items_json, + + # Старые поля (для обратной совместимости) "shipping_address_id": order.shipping_address_id, "shipping_address": shipping_address, + + # Дополнительная информация "payment_method": order.payment_method, "payment_details": order.payment_details, "tracking_number": order.tracking_number, @@ -586,4 +787,6 @@ def get_order_with_details(db: Session, order_id: int) -> Optional[Dict]: "created_at": order.created_at, "updated_at": order.updated_at, "items": items - } \ No newline at end of file + } + + return result \ No newline at end of file diff --git a/backend/app/repositories/user_repo.py b/backend/app/repositories/user_repo.py index 3c67287..c96b4ad 100644 --- a/backend/app/repositories/user_repo.py +++ b/backend/app/repositories/user_repo.py @@ -1,7 +1,9 @@ from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from fastapi import HTTPException, status -from typing import List, Optional +from typing import List, Optional, Dict, Any, Tuple +import secrets +import string from app.models.user_models import User, UserAddress from app.schemas.user_schemas import UserCreate, UserUpdate, AddressCreate, AddressUpdate @@ -33,7 +35,7 @@ def create_user(db: Session, user: UserCreate) -> User: status_code=status.HTTP_400_BAD_REQUEST, detail="Пользователь с таким email уже существует" ) - + # Создаем нового пользователя hashed_password = get_password_hash(user.password) db_user = User( @@ -45,7 +47,7 @@ def create_user(db: Session, user: UserCreate) -> User: is_active=user.is_active, is_admin=user.is_admin ) - + try: db.add(db_user) db.commit() @@ -66,17 +68,17 @@ def update_user(db: Session, user_id: int, user: UserUpdate) -> User: status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден" ) - + # Обновляем только предоставленные поля update_data = user.dict(exclude_unset=True) - + # Если предоставлен новый пароль, хешируем его if "password" in update_data and update_data["password"]: update_data["password"] = get_password_hash(update_data.pop("password")) - + # Удаляем поле password_confirm, если оно есть update_data.pop("password_confirm", None) - + # Проверяем уникальность email, если он изменяется if "email" in update_data and update_data["email"] != db_user.email: if get_user_by_email(db, update_data["email"]): @@ -84,11 +86,11 @@ def update_user(db: Session, user_id: int, user: UserUpdate) -> User: status_code=status.HTTP_400_BAD_REQUEST, detail="Пользователь с таким email уже существует" ) - + # Применяем обновления for key, value in update_data.items(): setattr(db_user, key, value) - + try: db.commit() db.refresh(db_user) @@ -108,7 +110,7 @@ def delete_user(db: Session, user_id: int) -> bool: status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден" ) - + try: db.delete(db_user) db.commit() @@ -130,6 +132,57 @@ def authenticate_user(db: Session, email: str, password: str) -> Optional[User]: return user +def find_or_create_user_from_order(db: Session, user_info: Dict[str, Any]) -> Tuple[User, bool, str]: + """ + Ищет пользователя по email или создает нового на основе данных заказа. + + Args: + db: Сессия базы данных + user_info: Информация о пользователе из заказа + + Returns: + Кортеж (пользователь, создан_новый, сообщение) + """ + email = user_info.get("email") + if not email: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email обязателен для создания пользователя" + ) + + # Ищем пользователя по email + existing_user = get_user_by_email(db, email) + if existing_user: + return existing_user, False, "Пользователь с таким email уже существует" + + # Генерируем случайный пароль + password = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(12)) + hashed_password = get_password_hash(password) + + # Создаем нового пользователя + new_user = User( + email=email, + password=hashed_password, + first_name=user_info.get("first_name", ""), + last_name=user_info.get("last_name", ""), + phone=user_info.get("phone", ""), + is_active=True, + is_admin=False + ) + + try: + db.add(new_user) + db.commit() + db.refresh(new_user) + return new_user, True, f"Создан новый пользователь с email {email}" + except IntegrityError: + db.rollback() + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Ошибка при создании пользователя" + ) + + # Функции для работы с адресами пользователей def get_address(db: Session, address_id: int) -> Optional[UserAddress]: return db.query(UserAddress).filter(UserAddress.id == address_id).first() @@ -146,7 +199,7 @@ def create_address(db: Session, address: AddressCreate, user_id: int): UserAddress.user_id == user_id, UserAddress.is_default == True ).update({"is_default": False}) - + db_address = UserAddress( user_id=user_id, address_line1=address.address_line1, @@ -157,7 +210,7 @@ def create_address(db: Session, address: AddressCreate, user_id: int): country=address.country, is_default=address.is_default ) - + try: db.add(db_address) db.commit() @@ -178,16 +231,16 @@ def update_address(db: Session, address_id: int, address: AddressUpdate, user_id UserAddress.id == address_id, UserAddress.user_id == user_id ).first() - + if not db_address: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Адрес не найден или не принадлежит пользователю" ) - + # Обновляем только предоставленные поля update_data = address.dict(exclude_unset=True) - + # Если адрес становится дефолтным, сбрасываем дефолтный статус у других адресов пользователя if "is_default" in update_data and update_data["is_default"]: db.query(UserAddress).filter( @@ -195,11 +248,11 @@ def update_address(db: Session, address_id: int, address: AddressUpdate, user_id UserAddress.id != address_id, UserAddress.is_default == True ).update({"is_default": False}) - + # Применяем обновления for key, value in update_data.items(): setattr(db_address, key, value) - + try: db.commit() db.refresh(db_address) @@ -217,13 +270,13 @@ def delete_address(db: Session, address_id: int, user_id: int) -> bool: UserAddress.id == address_id, UserAddress.user_id == user_id ).first() - + if not db_address: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Адрес не найден или не принадлежит пользователю" ) - + try: db.delete(db_address) db.commit() @@ -245,10 +298,10 @@ def update_password(db: Session, user_id: int, new_password: str) -> bool: status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден" ) - + hashed_password = get_password_hash(new_password) db_user.password = hashed_password - + try: db.commit() return True @@ -264,14 +317,14 @@ def create_password_reset_token(db: Session, user_id: int) -> str: """Создает токен для сброса пароля""" import secrets import datetime - + # В реальном приложении здесь должна быть модель для токенов сброса пароля # Для примера просто генерируем случайный токен token = secrets.token_urlsafe(32) - + # В реальном приложении сохраняем токен в базе данных с привязкой к пользователю # и временем истечения срока действия - + return token @@ -279,7 +332,7 @@ def verify_password_reset_token(db: Session, token: str) -> Optional[int]: """Проверяет токен сброса пароля и возвращает ID пользователя""" # В реальном приложении проверяем токен в базе данных # и его срок действия - + # Для примера просто возвращаем фиктивный ID пользователя # В реальном приложении это должна быть проверка в базе данных - return 1 # Фиктивный ID пользователя \ No newline at end of file + return 1 # Фиктивный ID пользователя \ No newline at end of file diff --git a/backend/app/routers/__pycache__/catalog_router.cpython-310.pyc b/backend/app/routers/__pycache__/catalog_router.cpython-310.pyc index 4de452d52b425a429b77428720f1cce900fd3fde..291f164fea19e761984c387badabde0ba83f98fb 100644 GIT binary patch delta 2192 zcmZ`)eQXp(6yMps+xzVGxVE$$?Qub%y%PFGTc~_Up@-Cz0@niqe|Saqs4b<1xh*K! z+R~y{L?aAfuoUhPf;CYp2T}PFP$C3@EvSSe1Py=q2gJk}G%~WxXJwX&3kX& zyqWiVyZg1Dx`?ASCB-VhbHCzY!}=FLacs^a5yrAw(6M6+j?2}@I2&x#BG*J^`V!NbCQG){7NR?vHr1b)=QjA<@P~?~h z^L*8)%1tIsQVgR6#q^fgMHKUYJf?9E&<%T_#@l51w@Q=h1fQk0Ef12?-`9`o1MI@o z?2f@r-s&oETRLdH`Vp^wQXd%X9PEM7J=momqYJ=5>j2suxbRbbKtB!x-h;2w=Rnmj zCTtOPh1wPgt!!1o30VoND;k^IMxgsC{g@YEK>r*zJf@%WS!o#bbQMAxKu5x%gqy=^ zIJB0wDs3xO+JGhlf((#IpBD@+43!oYg=SY)g#!LXi~V)MglkA!8me6o49#7zxV9?c z;kq*Ltga3E1GSYkp*nwHi9ZnX2LcNMJ_F^YD#p+y2rU3?YlV~bkIJ4E!5a`eU6P%+ zBqwnZ94^ry(s#h%BYeq95cwklT3{e0zJw9^ncXWXWxu&IIuxQj8b^{SK5;bdv z7>QamLz7h#R*N9pVnVx7Ge+j9b~UBRq1ZSz?0u4wg2h!+qfQOz0_2c4+98^wdBhr5 zy9uJ1G|~<9St_(-8w54&<#j}8Hxpr_QoDpLs)%W=3yE%u(P>E?u6K=B#f&X zX+#~aK7AYGc--z9+Js?tA*@E=Db<7Ivn0#PD~j9jvhp! zmD@S(F1SaE0K`taWR=X4D9S{BlqK6GH!%n zjm#-0F*ZME9eIs?k>gE%arW~sj_AorD}f?L%&|aC z0k>bVuf$G=i5_B$(h8VojGG+fftHN%rdMO2q=#LmAA;+~9JXR=W;`~gULH@kV?2g{ z=WW~p-gA9JT)}dHyd^$kLN^(E7@c+_AX{h+!g~n(0``E!9_CJz-$d0GR*;u#&ELCZ7o7M!WD!m00|2=QKWHWIFcf=){@!e8aq;Q z!-ZG=Pvi3Izs!0km%Fdyx+fUM%~#pI$sXf(V6J42(x+S~K>-WVe+$B4Hm7v6=`x5x z`YpRsT3Eu@VXTC?CA@*>K(C_f_Xs>q4wxpr0q{I4EXyD_n7^$3aiEK3g)&d-Fnd&% zS^B^0Z^HVuBJj};g2QB&97L2vXhwRIRhRE4*V+B@v7>H*u^YXtAlOX?Xj3aOi}Y4J zcgh5kx)V)9gtpMz&_dblrKx{H8?{x;8xcb*{$rpatOi2wuqzc?phnA{J_V)7oVLBA z2aNplPxqk|LpXqN2mu=;VNe?3pLGJ~rxDJe(zvR*b%jbVq2d|>-fqIl|4$`F#i2O? se`U~L#V?8CNhuzL-bT2Cki1NmH@)97!b8$?VN75%XJp2InO;o(21+J7KmY&$ delta 1449 zcmai!T})h65P)~?E*Dsq%g<6-D!VLLFq`FP7nkL)Kh{D+69tj2HI2y@xm#S_0y*~* zqur*64fMxS=|D^^w1rqn(}%`v(xx_5W0P9jP@$=%xepC*CML#*KKRn8GZzrwyvh0Q zoH=vm%$Ygo{5lM?Nu@tK+isz6?w4o1d-i>;9C5)@L|30YN=h+|i1Q~5WBS^D~IE7VYU*V}`uq5q~e< zicd;>+l>6SNaXcsC=iOacSNE)J9f9Xm~n`cBsoR+bw3Q5U zB@Nj#WN?6(!b_zId74yW#jK&so0CswPdUNTkdiLWbG2}?Y$HsYj^@dtm>sQHIAk%K zdRCT{GpSS?Y0}^Zyi&5kv#Z4`KBI1Yf^2tE=FKYb&PX0lP5V9pn8OZtz#%>-rIEHj zE;Joq@%!b|)EDEZ`CrU3OLCF>U*Sc+3gu|8+yhtfoys!XWm4lpsUK67F8LbUT&4h< zvy!x+xWN7EX6UU|F1SV`9!yrbvqpHdkw`uh~=SnN{t@!f(<7_N8t*3>fCS-H`jFq?y;u08a=)h*;Vx9|^>Pl!Yf7UJK&yjlVMzAdI;Bq(&_xX^?rs6IJ>OE--G**CJ z^)KYJL=~%H<8j6u9<4u;euu<}_!*rIHErfR-n)^}7yG~rLfmE3MTQxt364`dAOz_a zYYq||vcsx?WR*bvOFY2i4Tt`9tTfhi-scJH-W`JjYPTL!|7V4VG;7D&3QGEaR(ObG zjkC1bU{hJqBeHsvt@z1uq}!|xutSC<9;FgZ6_E2T53Qc?Oz}81)9eDeRNriF%hP#i zt<8 diff --git a/backend/app/routers/__pycache__/order_router.cpython-310.pyc b/backend/app/routers/__pycache__/order_router.cpython-310.pyc index 6507f2dfb8db375361dacf6226b9417d0fbd678f..83202f2e1685e9e1b0902b75fd2d08fc25129a04 100644 GIT binary patch delta 2276 zcmaJ?O>7%Q6!y%n*NMIMI*IddC!4lOvuWZ)RoaSDB1#(xRMh`)SSn1&Oj<`?JB)3r zl$89ns8WeaHL8T*QV$9zvD7Y+w~@RHKH?)dqd5UPAje9k%Bi-iouD0b zLUzc}ZQTjmVY#MeB937jPSlRdxj$3m)Y`RjtYzw)m>qNK?RuxdZg3jyMrjXZnw)04 zMUJa7txnvIBOgVin#=?*A`)7}c7i4*+K5g=w4LhXxYke zl+;}FFCg2L{{-(OrX6*-cLG! z{4Qv_X^_UjZ(}1tvWIj^yl!Hi^I2Vgh&r(RUiHz_Z}AB3s`J!0a%9RvUf>+f=hL~Y z<>yCnYryl5WMRfH-~puaMcfhfBBvOk>=>h?1$vTR=0D>14q)=Z7#5h7M~wZ@o!@QLY^1lLm zmcLUjpafbWJ26rg$F=wOK3vulKU7MyputhzR&FGDds6lrL*I?o}R;| z`vtJuQuI@IwR8(00l#qusC5IQwbCu%at(0SU#`2zTAu%t2@- z{SjqT|C2&+1?u8tLBhu}*%}~$5blFs5M{)D0E(=X=3uh=uPn`D$A?dBB~2Holb0F4 zDT^p7#&qv*7h;7!Q(o&B%Q+6s7NqZ&;JXID6-Xhg43a(K2{k!$vUm(?V_57IByoQ# z-4aS}Y}s8GJ9r}H+EfsvfU}SqZWG_aUMOD}Efn*qu^geEMrZ**t$8fCtJYL07^w*NBke^*V|!ugN~r&1d7n~ga@DKa7{XmEG6q7X zL_+?H{}6V3JghfyQ%iCqw69Ms)2+Tp3*`&x;WY88()rXVanjj)3hM@VW{Iwqf3I~J zLxPDA0nf%lEXjY{X9rKCiH$3cizoQ+!A?ln*IfCejAqAZW_zwaJ`%Ew(JjA zkl2V6jveItBb`(tXlziJ4~S0GM@2Lkd73Qf;u_lkZZd3LA=0WFRH81|-@vIh& zp%|`Jn2_AE_*x|K4%1-lMe-kJMl)lVs56z!F^2_&Q7cSMrn47W6--#Q=mZQ4iB5;k zs~Vl0OpfIkWnp292%7>nX7Emtv8F3Wj8KWE3^E*aIr>rzj0n(lV zCCQQ!V6s?X10rC*=!W>OM%UoyFpT?j6h~34qb8)oFQUY-rmD{|!hd5$e;%vR6!nFi NVf{rc!MmgH{{vtM=0pGh delta 1330 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 Cck-(M diff --git a/backend/app/schemas/order_schemas.py b/backend/app/schemas/order_schemas.py index d9939a7..9a1b1b3 100644 --- a/backend/app/schemas/order_schemas.py +++ b/backend/app/schemas/order_schemas.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, Field +from pydantic import BaseModel, field_validator from typing import Optional, List, Dict, Any from datetime import datetime @@ -62,7 +62,107 @@ class OrderItemWithProduct(OrderItem): variant_name: Optional[str] = None -# Схемы для заказов +# Схемы для информации о пользователе +class UserInfoBase(BaseModel): + first_name: str + last_name: Optional[str] = "" + email: str + phone: str + + +# Схемы для адреса доставки +class AddressBase(BaseModel): + city: str + street: Optional[str] = "" + house: Optional[str] = "" + apartment: Optional[str] = "" + postal_code: Optional[str] = "" + formatted_address: Optional[str] = "" + + +# Схемы для информации о доставке CDEK +class CDEKPvzInfo(BaseModel): + city_code: Optional[int] = None + city: Optional[str] = None + type: Optional[str] = None + postal_code: Optional[str] = None + country_code: Optional[str] = None + region: Optional[str] = None + have_cashless: Optional[bool] = None + have_cash: Optional[bool] = None + allowed_cod: Optional[bool] = None + is_dressing_room: Optional[bool] = None + code: Optional[str] = None + name: Optional[str] = None + address: Optional[str] = None + work_time: Optional[str] = None + location: Optional[List[float]] = None + weight_min: Optional[int] = None + weight_max: Optional[int] = None + dimensions: Optional[Any] = None + + +class CDEKTariffInfo(BaseModel): + tariff_code: Optional[int] = None + tariff_name: Optional[str] = None + tariff_description: Optional[str] = None + delivery_mode: Optional[int] = None + delivery_sum: Optional[int] = None + period_min: Optional[int] = None + period_max: Optional[int] = None + calendar_min: Optional[int] = None + calendar_max: Optional[int] = None + delivery_date_range: Optional[Dict[str, str]] = None + + +class CDEKInfo(BaseModel): + pvz: Optional[CDEKPvzInfo] = None + tariff: Optional[CDEKTariffInfo] = None + delivery_type: Optional[str] = None + + +# Схемы для информации о доставке курьером +class CourierInfo(BaseModel): + geo_lat: Optional[str] = None + geo_lon: Optional[str] = None + fias_id: Optional[str] = None + kladr_id: Optional[str] = None + + +# Схемы для информации о доставке +class DeliveryInfo(BaseModel): + method: str # cdek или courier + address: AddressBase + cdek_info: Optional[CDEKInfo] = None + + +# Схемы для элементов заказа в новом формате +class OrderItemNew(BaseModel): + product_id: int + variant_id: int + quantity: int + price: float + + +# Схемы для заказов в новом формате +class OrderCreateNew(BaseModel): + user_info: UserInfoBase + delivery: DeliveryInfo + items: List[OrderItemNew] + payment_method: str + comment: Optional[str] = "" + + @field_validator('payment_method') + def validate_payment_method(cls, v): + # Проверяем, что значение payment_method соответствует одному из допустимых значений + valid_methods = ["sbp", "card"] + if v.lower() not in valid_methods: + # Если не соответствует, преобразуем к значению по умолчанию + return "card" + return v.lower() # Возвращаем значение в нижнем регистре + + +# Старые схемы для заказов (для обратной совместимости) class OrderBase(BaseModel): shipping_address_id: Optional[int] = None payment_method: Optional[PaymentMethod] = None @@ -82,12 +182,35 @@ class OrderUpdate(BaseModel): tracking_number: Optional[str] = None notes: Optional[str] = None + # Новые поля + delivery_method: Optional[str] = None + city: Optional[str] = None + delivery_address: Optional[str] = None + cdek_info: Optional[Dict[str, Any]] = None + courier_info: Optional[Dict[str, Any]] = None + user_info_json: Optional[Dict[str, Any]] = None + class Order(OrderBase): id: int user_id: int status: OrderStatus total_amount: float + + # Информация о пользователе (из JSON) + user_info_json: Optional[Dict[str, Any]] = None + + # Информация о доставке + delivery_method: Optional[str] = None + city: Optional[str] = None + delivery_address: Optional[str] = None + cdek_info: Optional[Dict[str, Any]] = None + courier_info: Optional[Dict[str, Any]] = None + + # Информация о товарах (из JSON) + items_json: Optional[List[Dict[str, Any]]] = None + + # Дополнительная информация payment_details: Optional[str] = None tracking_number: Optional[str] = None created_at: datetime @@ -119,7 +242,25 @@ class OrderWithDetails(BaseModel): user_id: int status: OrderStatus total_amount: float + + # Информация о пользователе (из JSON) + user_info_json: Optional[Dict[str, Any]] = None + + # Информация о доставке + delivery_method: Optional[str] = None + city: Optional[str] = None + delivery_address: Optional[str] = None + cdek_info: Optional[Dict[str, Any]] = None + courier_info: Optional[Dict[str, Any]] = None + + # Информация о товарах (из JSON) + items_json: Optional[List[Dict[str, Any]]] = None + + # Старые поля (для обратной совместимости) shipping_address_id: Optional[int] = None + shipping_address: Optional[Dict[str, Any]] = None + + # Дополнительная информация payment_method: Optional[PaymentMethod] = None payment_details: Optional[str] = None tracking_number: Optional[str] = None @@ -127,5 +268,4 @@ class OrderWithDetails(BaseModel): created_at: datetime updated_at: Optional[datetime] = None user_email: Optional[str] = None - shipping_address: Optional[Dict[str, Any]] = None - items: List[Dict[str, Any]] = [] \ No newline at end of file + items: List[Dict[str, Any]] = [] \ No newline at end of file diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py index 6d2ce0a..080e0fe 100644 --- a/backend/app/services/__init__.py +++ b/backend/app/services/__init__.py @@ -16,7 +16,7 @@ from app.services.catalog_service import ( from app.services.order_service import ( add_to_cart, update_cart_item, remove_from_cart, clear_cart, get_cart, - create_order, get_order, update_order, cancel_order, + create_order, create_order_new, get_order, update_order, cancel_order, ) from app.services.review_service import ( diff --git a/backend/app/services/__pycache__/__init__.cpython-310.pyc b/backend/app/services/__pycache__/__init__.cpython-310.pyc index 841da53a8642de36f6449ecc67b5e6fdebd3e894..e0cc695acceecefcd7840b5850a681e68969e2e4 100644 GIT binary patch delta 255 zcmX@g|A3!2pO=@50SIz5cr%tw` zG}kQ3EZ02B94yD0W07kaWtnRgWtD3kWu0piWs_?gWt(dkWe1jL%dyXOh;jh4*>fCo zouZsJ2QY4BV&t04#u6qdkX)3SSdto_UzCzs6rYz`J~^7@H>30Ba8_$ZrXqvM{cMV& zhCr7U8G#665Mcr$OeZg76`p*9jeqhRHVLL8>&cAl$|^QMnJ9t8f&#sw)Pnrt%#!?~ h%+%r{TcGeth9WzVKD)^t?8RILTr#W*EIh0NEC8G5L4g1O delta 211 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 delta 2898 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 L^g``av2pxw8d8aO diff --git a/backend/app/services/catalog_service.py b/backend/app/services/catalog_service.py index c3f6360..38222e8 100644 --- a/backend/app/services/catalog_service.py +++ b/backend/app/services/catalog_service.py @@ -308,8 +308,10 @@ def update_product(db: Session, product_id: int, product: ProductUpdate) -> Dict def delete_product(db: Session, product_id: int) -> Dict[str, Any]: """Удалить продукт""" try: + logging.warning(f"Удаление продукта с ID {product_id}") # Проверяем, что продукт существует db_product = catalog_repo.get_product(db, product_id) + logging.warning(f"Продукт: {db_product}, {db_product.id if db_product else 'не найден'}") if not db_product: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -318,7 +320,7 @@ def delete_product(db: Session, product_id: int) -> Dict[str, Any]: # Удаляем продукт success = catalog_repo.delete_product(db, product_id) - + logging.warning(f"Удаление продукта с ID {product_id} успешно: {success}") # Удаляем продукт из Meilisearch if success: meilisearch_service.delete_product(product_id) @@ -327,11 +329,14 @@ def delete_product(db: Session, product_id: int) -> Dict[str, Any]: "success": success } except HTTPException as e: + logging.error(f"HTTP ошибка при удалении продукта с ID {product_id}: {e.detail}") return { "success": False, "error": e.detail } except Exception as e: + logging.error(f"Неожиданная ошибка при удалении продукта с ID {product_id}: {str(e)}") + logging.error(traceback.format_exc()) return { "success": False, "error": str(e) @@ -990,6 +995,10 @@ def create_product_complete(db: Session, product_data: ProductCreateComplete) -> "updated_at": image.updated_at }) + # Индексируем продукт в Meilisearch + product_data = format_product_for_meilisearch(db_product, variants, images) + meilisearch_service.index_product(product_data) + return { "success": True, "id": db_product.id, @@ -1150,20 +1159,27 @@ def update_product_complete(db: Session, product_id: int, product_data: ProductU # Коммитим транзакцию db.commit() + # Обновляем продукт в Meilisearch + # Получаем все варианты и изображения продукта после обновления + updated_variants = db_product.variants + updated_images = db_product.images + product_data = format_product_for_meilisearch(db_product, updated_variants, updated_images) + meilisearch_service.index_product(product_data) + # Собираем полный ответ - product_schema = Product.from_orm(db_product) + product_schema = Product.model_validate(db_product) return { "success": True, "product": product_schema, "variants": { - "updated": [ProductVariant.from_orm(variant) for variant in variants_updated], - "created": [ProductVariant.from_orm(variant) for variant in variants_created], + "updated": [ProductVariant.model_validate(variant) for variant in variants_updated], + "created": [ProductVariant.model_validate(variant) for variant in variants_created], "removed": variants_removed }, "images": { - "updated": [ProductImage.from_orm(image) for image in images_updated], - "created": [ProductImage.from_orm(image) for image in images_created], + "updated": [ProductImage.model_validate(image) for image in images_updated], + "created": [ProductImage.model_validate(image) for image in images_created], "removed": images_removed } } diff --git a/backend/app/services/order_service.py b/backend/app/services/order_service.py index 6b3ccd4..9bcb770 100644 --- a/backend/app/services/order_service.py +++ b/backend/app/services/order_service.py @@ -1,9 +1,9 @@ from sqlalchemy.orm import Session from fastapi import HTTPException, status -from typing import Dict, Any, List +from typing import Dict, Any, List, Optional from app.repositories import order_repo, content_repo -from app.schemas.order_schemas import CartItemCreate, CartItemUpdate, OrderCreate, OrderUpdate, Order +from app.schemas.order_schemas import CartItemCreate, CartItemUpdate, OrderCreate, OrderUpdate, Order, OrderCreateNew from app.schemas.content_schemas import AnalyticsLogCreate from app.models.order_models import OrderStatus @@ -14,7 +14,7 @@ def add_to_cart(db: Session, user_id: int, cart_item: CartItemCreate) -> Dict[st Добавляет товар в корзину пользователя. """ new_cart_item = order_repo.create_cart_item(db, cart_item, user_id) - + # Логируем событие добавления в корзину log_data = AnalyticsLogCreate( user_id=user_id, @@ -25,7 +25,7 @@ def add_to_cart(db: Session, user_id: int, cart_item: CartItemCreate) -> Dict[st } ) content_repo.log_analytics_event(db, log_data) - + return { "success": True, "message": "Товар успешно добавлен в корзину", @@ -43,7 +43,7 @@ def update_cart_item(db: Session, user_id: int, cart_item_id: int, cart_item: Ca Обновляет количество товара в корзине пользователя. """ updated_cart_item = order_repo.update_cart_item(db, cart_item_id, cart_item, user_id) - + return { "success": True, "message": "Товар в корзине успешно обновлен", @@ -61,7 +61,7 @@ def remove_from_cart(db: Session, user_id: int, cart_item_id: int) -> Dict[str, Удаляет товар из корзины пользователя. """ success = order_repo.delete_cart_item(db, cart_item_id, user_id) - + return { "success": success, "message": "Товар успешно удален из корзины" if success else "Не удалось удалить товар из корзины" @@ -73,7 +73,7 @@ def clear_cart(db: Session, user_id: int) -> Dict[str, Any]: Очищает корзину пользователя. """ success = order_repo.clear_cart(db, user_id) - + return { "success": success, "message": "Корзина успешно очищена" if success else "Не удалось очистить корзину" @@ -86,10 +86,10 @@ def get_cart(db: Session, user_id: int) -> Dict[str, Any]: """ # Получаем элементы корзины с деталями продуктов cart_items = order_repo.get_cart_with_product_details(db, user_id) - + # Рассчитываем общую сумму корзины total_amount = sum(item["total_price"] for item in cart_items) - + return { "items": cart_items, "total_amount": total_amount, @@ -98,13 +98,13 @@ def get_cart(db: Session, user_id: int) -> Dict[str, Any]: # Сервисы заказов -def create_order(db: Session, user_id: int, order: OrderCreate) -> Dict[str, Any]: +def create_order(db: Session, user_id: Optional[int], order: OrderCreate) -> Dict[str, Any]: """ Создает новый заказ на основе корзины или переданных элементов. """ try: new_order = order_repo.create_order(db, order, user_id) - + # Логируем событие создания заказа log_data = AnalyticsLogCreate( user_id=user_id, @@ -115,10 +115,10 @@ def create_order(db: Session, user_id: int, order: OrderCreate) -> Dict[str, Any } ) content_repo.log_analytics_event(db, log_data) - + # Получаем заказ с деталями order_details = order_repo.get_order_with_details(db, new_order.id) - + return { "success": True, "message": "Заказ успешно создан", @@ -131,26 +131,103 @@ def create_order(db: Session, user_id: int, order: OrderCreate) -> Dict[str, Any ) +def create_order_new(db: Session, user_id: Optional[int], order: OrderCreateNew) -> Dict[str, Any]: + """ + Создает новый заказ на основе новой структуры данных. + Если пользователь не авторизован (user_id=None), создает нового пользователя. + + Args: + db: Сессия базы данных + user_id: ID пользователя (None, если пользователь не авторизован) + order: Данные для создания заказа + + Returns: + Словарь с информацией о созданном заказе + + Raises: + HTTPException: Если произошла ошибка при создании заказа + """ + try: + + print(f"Метод оплаты: {order.payment_method} {type(order.payment_method)} {order.payment_method.strip().lower()}") + # Проверяем метод оплаты + if order.payment_method.strip().lower() not in ["sbp", "card"]: + # Если метод оплаты не поддерживается, используем значение по умолчанию + order.payment_method = "card" + print(f"Неизвестный метод оплаты: {order.payment_method}. Используем значение по умолчанию: card") + + # Создаем заказ с новой структурой данных + new_order = order_repo.create_order_new(db, order, user_id) + + # Получаем сообщение о создании пользователя, если оно есть + user_created_message = getattr(new_order, "user_created_message", None) + + # Логируем событие создания заказа + log_data = AnalyticsLogCreate( + user_id=new_order.user_id, # Используем ID пользователя из заказа + event_type="order_created", + additional_data={ + "order_id": new_order.id, + "total_amount": new_order.total_amount, + "delivery_method": new_order.delivery_method + } + ) + content_repo.log_analytics_event(db, log_data) + + # Получаем заказ с деталями + order_details = order_repo.get_order_with_details(db, new_order.id) + + # Формируем ответ для фронтенда + response = { + "success": True, + "message": "Заказ успешно создан", + "order": order_details, + "order_id": new_order.id # Добавляем ID заказа для удобства + } + + # Если был создан новый пользователь, добавляем сообщение об этом + if user_created_message: + response["user_message"] = user_created_message + + return response + except Exception as e: + # Логируем ошибку для отладки + print(f"Ошибка при создании заказа: {str(e)}") + + # Проверяем, содержит ли ошибка информацию о проблеме с методом оплаты + error_message = str(e) + if "invalid input value for enum paymentmethod" in error_message.lower(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Неверный метод оплаты. Допустимые значения: 'sbp', 'card'" + ) + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Ошибка при создании заказа: {str(e)}" + ) + + def get_order(db: Session, user_id: int, order_id: int, is_admin: bool = False) -> Dict[str, Any]: """ Получает информацию о заказе по ID. """ # Получаем заказ с деталями order_details = order_repo.get_order_with_details(db, order_id) - + if not order_details: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Заказ не найден" ) - + # Проверяем права доступа if not is_admin and order_details["user_id"] != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Недостаточно прав для просмотра этого заказа" ) - + return { "success": True, "order": order_details @@ -168,27 +245,27 @@ def update_order(db: Session, user_id: int, order_id: int, order_update: OrderUp status_code=status.HTTP_404_NOT_FOUND, detail="Заказ не найден" ) - + # Проверяем права доступа if not is_admin and order.user_id != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Недостаточно прав для обновления этого заказа" ) - + # Обычные пользователи могут только отменить заказ if not is_admin and (order_update.status and order_update.status != OrderStatus.CANCELLED): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Пользователи могут только отменить заказ" ) - + # Обновляем заказ - updated_order = order_repo.update_order(db, order_id, order_update, is_admin) - + order_repo.update_order(db, order_id, order_update, is_admin) + # Получаем обновленный заказ с деталями order_details = order_repo.get_order_with_details(db, order_id) - + return { "success": True, "message": "Заказ успешно обновлен", @@ -207,21 +284,21 @@ def cancel_order(db: Session, user_id: int, order_id: int) -> Dict[str, Any]: status_code=status.HTTP_404_NOT_FOUND, detail="Заказ не найден" ) - + # Проверяем права доступа if order.user_id != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Недостаточно прав для отмены этого заказа" ) - + # Отменяем заказ order_update = OrderUpdate(status=OrderStatus.CANCELLED) order_repo.update_order(db, order_id, order_update, False) - + # Получаем обновленный заказ с деталями order_details = order_repo.get_order_with_details(db, order_id) - + # Логируем событие отмены заказа log_data = AnalyticsLogCreate( user_id=user_id, @@ -231,9 +308,9 @@ def cancel_order(db: Session, user_id: int, order_id: int) -> Dict[str, Any]: } ) content_repo.log_analytics_event(db, log_data) - + return { "success": True, "message": "Заказ успешно отменен", "order": order_details - } \ No newline at end of file + } \ No newline at end of file diff --git a/backend/docs/api_documentation.md b/backend/docs/api_documentation.md index a6fd9bd..e841fd2 100644 --- a/backend/docs/api_documentation.md +++ b/backend/docs/api_documentation.md @@ -9,31 +9,32 @@ 6. [Отзывы](#отзывы) 7. [Контент](#контент) 8. [Аналитика](#аналитика) +9. [Доставка](#доставка) ## Аутентификация Базовый URL: `/auth` -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| POST | `/register` | Регистрация нового пользователя | `UserCreate` (email, password, first_name, last_name) | Нет | -| POST | `/login` | Вход в систему | `username` (email), `password` | Нет | -| POST | `/reset-password` | Запрос на сброс пароля | `email` | Нет | -| POST | `/set-new-password` | Установка нового пароля по токену | `token`, `password` | Нет | -| POST | `/change-password` | Изменение пароля | `current_password`, `new_password` | Да | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| POST | `/register` | Регистрация нового пользователя | `UserCreate` (email, password, first_name, last_name) | Нет | `{"success": true, "user": {...}}` | +| POST | `/login` | Вход в систему | `username` (email), `password` | Нет | `{"access_token": "...", "token_type": "bearer"}` | +| POST | `/reset-password` | Запрос на сброс пароля | `email` | Нет | `{"success": true, "message": "..."}` | +| POST | `/set-new-password` | Установка нового пароля по токену | `token`, `password` | Нет | `{"success": true, "message": "..."}` | +| POST | `/change-password` | Изменение пароля | `current_password`, `new_password` | Да | `{"success": true, "message": "..."}` | ## Пользователи Базовый URL: `/users` -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| GET | `/me` | Получение профиля текущего пользователя | - | Да | -| PUT | `/me` | Обновление профиля текущего пользователя | `UserUpdate` (first_name, last_name, phone) | Да | -| POST | `/me/addresses` | Добавление адреса пользователя | `AddressCreate` (city, street, house, apartment, postal_code, is_default) | Да | -| PUT | `/me/addresses/{address_id}` | Обновление адреса пользователя | `AddressUpdate` (city, street, house, apartment, postal_code, is_default) | Да | -| DELETE | `/me/addresses/{address_id}` | Удаление адреса пользователя | - | Да | -| GET | `/{user_id}` | Получение профиля пользователя по ID (только для админов) | - | Да (админ) | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| GET | `/me` | Получение профиля текущего пользователя | - | Да | `{"success": true, "user": {...}}` | +| PUT | `/me` | Обновление профиля текущего пользователя | `UserUpdate` (first_name, last_name, phone) | Да | `{"success": true, "user": {...}}` | +| POST | `/me/addresses` | Добавление адреса пользователя | `AddressCreate` (city, street, house, apartment, postal_code, is_default) | Да | `{"success": true, "address": {...}}` | +| PUT | `/me/addresses/{address_id}` | Обновление адреса пользователя | `AddressUpdate` (city, street, house, apartment, postal_code, is_default) | Да | `{"success": true, "address": {...}}` | +| DELETE | `/me/addresses/{address_id}` | Удаление адреса пользователя | - | Да | `{"success": true, "message": "..."}` | +| GET | `/{user_id}` | Получение профиля пользователя по ID (только для админов) | - | Да (админ) | `{"success": true, "user": {...}}` | ## Каталог @@ -41,117 +42,127 @@ ### Коллекции -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| GET | `/collections` | Получение списка коллекций | `skip`, `limit` | Нет | -| POST | `/collections` | Создание новой коллекции | `CollectionCreate` (name, slug, description, is_active) | Да (админ) | -| PUT | `/collections/{collection_id}` | Обновление коллекции | `CollectionUpdate` (name, slug, description, is_active) | Да (админ) | -| DELETE | `/collections/{collection_id}` | Удаление коллекции | - | Да (админ) | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| GET | `/collections` | Получение списка коллекций | `skip`, `limit`, `search`, `is_active` | Нет | `{"success": true, "collections": [...], "total": number}` | +| POST | `/collections` | Создание новой коллекции | `CollectionCreate` (name, slug, description, is_active) | Да (админ) | `{"success": true, "collection": {...}}` | +| PUT | `/collections/{collection_id}` | Обновление коллекции | `CollectionUpdate` (name, slug, description, is_active) | Да (админ) | `{"success": true, "collection": {...}}` | +| DELETE | `/collections/{collection_id}` | Удаление коллекции | - | Да (админ) | `{"success": true, "message": "..."}` | ### Категории -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| GET | `/categories` | Получение дерева категорий | - | Нет | -| POST | `/categories` | Создание новой категории | `CategoryCreate` (name, slug, description, parent_id, is_active) | Да (админ) | -| PUT | `/categories/{category_id}` | Обновление категории | `CategoryUpdate` (name, slug, description, parent_id, is_active) | Да (админ) | -| DELETE | `/categories/{category_id}` | Удаление категории | - | Да (админ) | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| GET | `/categories` | Получение списка категорий | `skip`, `limit`, `search`, `parent_id`, `is_active` | Нет | `{"success": true, "categories": [...], "total": number}` | +| POST | `/categories` | Создание новой категории | `CategoryCreate` (name, slug, description, parent_id, is_active) | Да (админ) | `{"success": true, "category": {...}}` | +| PUT | `/categories/{category_id}` | Обновление категории | `CategoryUpdate` (name, slug, description, parent_id, is_active) | Да (админ) | `{"success": true, "category": {...}}` | +| DELETE | `/categories/{category_id}` | Удаление категории | - | Да (админ) | `{"success": true, "message": "..."}` | ### Размеры -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| GET | `/sizes` | Получение списка размеров | `skip`, `limit` | Нет | -| GET | `/sizes/{size_id}` | Получение размера по ID | - | Нет | -| POST | `/sizes` | Создание нового размера | `SizeCreate` (name, code, description) | Да (админ) | -| PUT | `/sizes/{size_id}` | Обновление размера | `SizeUpdate` (name, code, description) | Да (админ) | -| DELETE | `/sizes/{size_id}` | Удаление размера | - | Да (админ) | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| GET | `/sizes` | Получение списка размеров | `skip`, `limit`, `search` | Нет | `{"success": true, "sizes": [...], "total": number}` | +| GET | `/sizes/{size_id}` | Получение размера по ID | - | Нет | `{"success": true, "size": {...}}` | +| POST | `/sizes` | Создание нового размера | `SizeCreate` (name, code, description) | Да (админ) | `{"success": true, "size": {...}}` | +| PUT | `/sizes/{size_id}` | Обновление размера | `SizeUpdate` (name, code, description) | Да (админ) | `{"success": true, "size": {...}}` | +| DELETE | `/sizes/{size_id}` | Удаление размера | - | Да (админ) | `{"success": true}` | ### Продукты -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| GET | `/products` | Получение списка продуктов | `skip`, `limit`, `category_id`, `collection_id`, `search`, `min_price`, `max_price`, `is_active`, `include_variants` | Нет | -| GET | `/products/{product_id}` | Получение детальной информации о продукте | - | Нет | -| GET | `/products/slug/{slug}` | Получение продукта по slug | - | Нет | -| POST | `/products` | Создание нового продукта | `ProductCreate` (name, slug, description, price, discount_price, care_instructions, is_active, category_id, collection_id) | Да (админ) | -| PUT | `/products/{product_id}` | Обновление продукта | `ProductUpdate` (name, slug, description, price, discount_price, care_instructions, is_active, category_id, collection_id) | Да (админ) | -| DELETE | `/products/{product_id}` | Удаление продукта | - | Да (админ) | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| GET | `/products` | Получение списка продуктов | `skip`, `limit`, `category_id`, `collection_id`, `search`, `min_price`, `max_price`, `is_active`, `sort_by`, `sort_order` | Нет | `{"success": true, "products": [...], "total": number, "skip": number, "limit": number}` | +| GET | `/products/{product_id}` | Получение детальной информации о продукте | - | Нет | `{"success": true, "product": {...}}` | +| GET | `/products/slug/{slug}` | Получение продукта по slug | - | Нет | `{"success": true, "product": {...}}` | +| POST | `/products` | Создание нового продукта | `ProductCreate` (name, slug, description, price, discount_price, care_instructions, is_active, category_id, collection_id) | Да (админ) | `{"success": true, "product": {...}}` | +| POST | `/products/complete` | Создание продукта с вариантами и изображениями | `ProductCreateComplete` (product + variants + images) | Да (админ) | `{"success": true, "product": {...}}` | +| PUT | `/products/{product_id}` | Обновление продукта | `ProductUpdate` (name, slug, description, price, discount_price, care_instructions, is_active, category_id, collection_id) | Да (админ) | `{"success": true, "product": {...}}` | +| PUT | `/products/{product_id}/complete` | Обновление продукта с вариантами и изображениями | `ProductUpdateComplete` (product + variants + images) | Да (админ) | `{"success": true, "product": {...}, "variants": {...}, "images": {...}}` | +| DELETE | `/products/{product_id}` | Удаление продукта | - | Да (админ) | `{"success": true, "message": "..."}` | ### Варианты продуктов -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| POST | `/products/{product_id}/variants` | Добавление варианта продукта | `ProductVariantCreate` (product_id, size_id, sku, stock, is_active) | Да (админ) | -| PUT | `/variants/{variant_id}` | Обновление варианта продукта | `ProductVariantUpdate` (product_id, size_id, sku, stock, is_active) | Да (админ) | -| DELETE | `/variants/{variant_id}` | Удаление варианта продукта | - | Да (админ) | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| POST | `/products/{product_id}/variants` | Добавление варианта продукта | `ProductVariantCreate` (product_id, size_id, sku, stock, is_active) | Да (админ) | `{"success": true, "variant": {...}}` | +| PUT | `/variants/{variant_id}` | Обновление варианта продукта | `ProductVariantUpdate` (product_id, size_id, sku, stock, is_active) | Да (админ) | `{"success": true, "variant": {...}}` | +| DELETE | `/variants/{variant_id}` | Удаление варианта продукта | - | Да (админ) | `{"success": true, "message": "..."}` | ### Изображения продуктов -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| POST | `/products/{product_id}/images` | Загрузка изображения продукта | `file`, `is_primary` | Да (админ) | -| PUT | `/images/{image_id}` | Обновление изображения продукта | `ProductImageUpdate` (alt_text, is_primary) | Да (админ) | -| DELETE | `/images/{image_id}` | Удаление изображения продукта | - | Да (админ) | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| POST | `/products/{product_id}/images` | Загрузка изображения продукта | `file` (multipart/form-data), `is_primary` (form field) | Да (админ) | `{"success": true, "image": {...}}` | +| PUT | `/images/{image_id}` | Обновление изображения продукта | `ProductImageUpdate` (alt_text, is_primary) | Да (админ) | `{"success": true, "image": {...}}` | +| DELETE | `/images/{image_id}` | Удаление изображения продукта | - | Да (админ) | `{"success": true, "message": "..."}` | ## Корзина Базовый URL: `/cart` -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| GET | `/` | Получение корзины пользователя | - | Да | -| POST | `/items` | Добавление товара в корзину | `CartItemCreate` (product_variant_id, quantity) | Да | -| PUT | `/items/{cart_item_id}` | Обновление товара в корзине | `CartItemUpdate` (quantity) | Да | -| DELETE | `/items/{cart_item_id}` | Удаление товара из корзины | - | Да | -| DELETE | `/clear` | Очистка корзины | - | Да | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| GET | `/` | Получение корзины пользователя | - | Да | `{"success": true, "cart": {"items": [...], "items_count": number, "total_amount": number}}` | +| POST | `/items` | Добавление товара в корзину | `CartItemCreate` (variant_id, quantity) | Да | `{"success": true, "cart_item": {...}}` | +| PUT | `/items/{cart_item_id}` | Обновление товара в корзине | `CartItemUpdate` (quantity) | Да | `{"success": true, "cart_item": {...}}` | +| DELETE | `/items/{cart_item_id}` | Удаление товара из корзины | - | Да | `{"success": true, "message": "..."}` | +| DELETE | `/clear` | Очистка корзины | - | Да | `{"success": true, "message": "..."}` | ## Заказы Базовый URL: `/orders` -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| GET | `/` | Получение списка заказов | `skip`, `limit`, `status` | Да | -| GET | `/{order_id}` | Получение информации о заказе | - | Да | -| POST | `/` | Создание нового заказа | `OrderCreate` (shipping_address_id, payment_method) | Да | -| PUT | `/{order_id}` | Обновление заказа | `OrderUpdate` (status, tracking_number) | Да (админ) | -| POST | `/{order_id}/cancel` | Отмена заказа | - | Да | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| GET | `/` | Получение списка заказов | `skip`, `limit`, `status` | Да | `[{"id": number, "status": string, "total_amount": number, ...}]` | +| GET | `/{order_id}` | Получение информации о заказе | - | Да | `{"success": true, "order": {...}}` | +| POST | `/` | Создание нового заказа | `OrderCreate` (shipping_address_id, payment_method, notes, cart_items, items) | Да | `{"success": true, "order": {...}}` | +| PUT | `/{order_id}` | Обновление заказа | `OrderUpdate` (status, shipping_address_id, payment_method, payment_details, tracking_number, notes) | Да (админ) | `{"success": true, "order": {...}}` | +| POST | `/{order_id}/cancel` | Отмена заказа | - | Да | `{"success": true, "message": "..."}` | ## Отзывы Базовый URL: `/reviews` -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| GET | `/products/{product_id}` | Получение отзывов о продукте | `skip`, `limit` | Нет | -| POST | `/` | Создание нового отзыва | `ReviewCreate` (product_id, rating, text) | Да | -| PUT | `/{review_id}` | Обновление отзыва | `ReviewUpdate` (rating, text) | Да | -| DELETE | `/{review_id}` | Удаление отзыва | - | Да | -| POST | `/{review_id}/approve` | Одобрение отзыва | - | Да (админ) | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| GET | `/products/{product_id}` | Получение отзывов о продукте | `skip`, `limit` | Нет | `{"success": true, "reviews": [...], "total": number}` | +| POST | `/` | Создание нового отзыва | `ReviewCreate` (product_id, rating, text) | Да | `{"success": true, "review": {...}}` | +| PUT | `/{review_id}` | Обновление отзыва | `ReviewUpdate` (rating, text) | Да | `{"success": true, "review": {...}}` | +| DELETE | `/{review_id}` | Удаление отзыва | - | Да | `{"success": true, "message": "..."}` | +| POST | `/{review_id}/approve` | Одобрение отзыва | - | Да (админ) | `{"success": true, "review": {...}}` | ## Контент Базовый URL: `/content` -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| GET | `/pages` | Получение списка страниц | `skip`, `limit` | Нет | -| GET | `/pages/{page_id}` | Получение страницы по ID | - | Нет | -| GET | `/pages/slug/{slug}` | Получение страницы по slug | - | Нет | -| POST | `/pages` | Создание новой страницы | `PageCreate` (title, slug, content, is_published) | Да (админ) | -| PUT | `/pages/{page_id}` | Обновление страницы | `PageUpdate` (title, slug, content, is_published) | Да (админ) | -| DELETE | `/pages/{page_id}` | Удаление страницы | - | Да (админ) | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| GET | `/pages` | Получение списка страниц | `skip`, `limit` | Нет | `{"success": true, "pages": [...], "total": number}` | +| GET | `/pages/{page_id}` | Получение страницы по ID | - | Нет | `{"success": true, "page": {...}}` | +| GET | `/pages/slug/{slug}` | Получение страницы по slug | - | Нет | `{"success": true, "page": {...}}` | +| POST | `/pages` | Создание новой страницы | `PageCreate` (title, slug, content, is_published) | Да (админ) | `{"success": true, "page": {...}}` | +| PUT | `/pages/{page_id}` | Обновление страницы | `PageUpdate` (title, slug, content, is_published) | Да (админ) | `{"success": true, "page": {...}}` | +| DELETE | `/pages/{page_id}` | Удаление страницы | - | Да (админ) | `{"success": true, "message": "..."}` | ## Аналитика Базовый URL: `/analytics` -| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | -|-------|----------|----------|-------------------|------------------------| -| POST | `/log` | Логирование события аналитики | `AnalyticsLogCreate` (event_type, event_data, user_id) | Нет | -| GET | `/logs` | Получение логов аналитики | `event_type`, `start_date`, `end_date`, `skip`, `limit` | Да (админ) | -| GET | `/report` | Получение аналитического отчета | `report_type`, `start_date`, `end_date` | Да (админ) | +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| POST | `/log` | Логирование события аналитики | `AnalyticsLogCreate` (event_type, event_data, user_id) | Нет | `{"success": true, "log": {...}}` | +| GET | `/logs` | Получение логов аналитики | `event_type`, `start_date`, `end_date`, `skip`, `limit` | Да (админ) | `{"success": true, "logs": [...], "total": number}` | +| GET | `/report` | Получение аналитического отчета | `report_type`, `start_date`, `end_date` | Да (админ) | `{"success": true, "report": {...}}` | + +## Доставка + +Базовый URL: `/delivery` + +| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | +|-------|----------|----------|-------------------|------------------------|------------------| +| POST | `/cdek` | Обработка запросов виджета CDEK | `action` (offices, calculate) и другие параметры в зависимости от действия | Нет | Ответ от API CDEK | ## Модели данных @@ -174,12 +185,14 @@ - `ProductVariantUpdate`: product_id, size_id, sku, stock, is_active - `ProductImageCreate`: product_id, image_url, alt_text, is_primary - `ProductImageUpdate`: alt_text, is_primary +- `ProductCreateComplete`: включает данные продукта, список вариантов и изображений +- `ProductUpdateComplete`: включает данные продукта, список вариантов и изображений для обновления ### Корзина и заказы -- `CartItemCreate`: product_variant_id, quantity +- `CartItemCreate`: variant_id, quantity - `CartItemUpdate`: quantity -- `OrderCreate`: shipping_address_id, payment_method -- `OrderUpdate`: status, tracking_number +- `OrderCreate`: shipping_address_id, payment_method, notes, cart_items, items +- `OrderUpdate`: status, shipping_address_id, payment_method, payment_details, tracking_number, notes ### Отзывы - `ReviewCreate`: product_id, rating, text @@ -188,4 +201,4 @@ ### Контент - `PageCreate`: title, slug, content, is_published - `PageUpdate`: title, slug, content, is_published -- `AnalyticsLogCreate`: event_type, event_data, user_id \ No newline at end of file +- `AnalyticsLogCreate`: event_type, event_data, user_id \ No newline at end of file diff --git a/backend/requirements-new.txt b/backend/requirements-new.txt new file mode 100644 index 0000000..777e2ae --- /dev/null +++ b/backend/requirements-new.txt @@ -0,0 +1,169 @@ +# Основные зависимости +fastapi==0.109.0 +uvicorn==0.34.0 +pydantic==2.5.3 +pydantic-settings==2.1.0 +pydantic_core==2.14.6 +starlette==0.35.1 +typing_extensions==4.12.2 + +# Валидация данных +marshmallow==3.21.1 +cerberus==1.3.5 +jsonschema==4.21.1 + +# База данных +sqlalchemy==2.0.25 +asyncpg==0.30.0 +psycopg2-binary==2.9.9 +alembic==1.13.1 +greenlet==3.0.3 # Требуется для SQLAlchemy с asyncpg + +# Аутентификация и безопасность +python-jose==3.3.0 +passlib==1.7.4 +bcrypt<4.0.0 +python-multipart==0.0.6 +email-validator==2.1.0 +pycryptodome==3.21.0 +pycryptodomex==3.21.0 +PyJWT==2.8.0 + +# HTTP и файлы +httpx==0.28.1 +aiofiles==24.1.0 +anyio==4.2.0 # Требуется для FastAPI и httpx + +# CORS +django-cors-headers==4.3.1 # Для Django проектов +flask-cors==4.0.0 # Для Flask проектов + +# WebSocket +websocket-client==1.8.0 +trio==0.28.0 +trio-websocket==0.11.1 +wsproto==1.2.0 +python-socketio==5.11.1 +websockets==12.0 + +# Поиск и индексация +meilisearch==0.28.0 + +# Облачное хранилище +boto3==1.34.69 + +# Кэширование +redis==5.0.3 +aioredis==2.0.1 + +# Очереди сообщений +amqp==5.3.1 +kombu==5.4.2 +aiokafka==0.10.0 + +# Асинхронные задачи +taskiq==0.11.10 +taskiq-dependencies==1.5.6 + +# Многопоточность и параллелизм +futures==3.1.1 +concurrent-log-handler==0.9.24 + +# API документация +openapi-spec-validator==0.7.1 +swagger-ui-bundle==0.0.9 +pydantic-openapi-schema==1.0.1 + +# GraphQL +strawberry-graphql==0.219.2 +graphene==3.3 +graphene-sqlalchemy==2.3.0 + +# Электронная почта +fastapi-mail==1.4.1 + +# Работа с датами и временем +python-dateutil==2.9.0.post0 +pytz==2025.1 + +# Интернационализация и локализация +babel==2.14.0 +python-gettext==5.0 +polib==1.2.0 + +# Анализ данных +pandas==2.2.3 +numpy==2.2.2 + +# Машинное обучение +scikit-learn==1.4.1 +joblib==1.3.2 + +# Обработка естественного языка (NLP) +nltk==3.8.1 +spacy==3.7.4 + +# Визуализация данных +matplotlib==3.8.4 +seaborn==0.13.2 +plotly==5.22.0 + +# Работа с Excel и другими форматами файлов +openpyxl==3.1.5 +xlrd==1.2.0 +XlsxWriter==3.2.2 + +# Работа с PDF и документами +pdfminer.six==20191110 +python-pptx==0.6.23 +PyPDF2==3.0.1 +reportlab==4.1.0 +python-docx==1.1.0 +docx2txt==0.8 + +# Работа с архивами +py7zr==0.22.0 +pyzstd==0.16.2 +rarfile==4.2 + +# Работа с XML и HTML +lxml==5.3.0 +beautifulsoup4==4.12.3 +soupsieve==2.6 + +# Веб-скрапинг +scrapy==2.11.1 +requests-html==0.10.0 + +# Работа с форматами данными +pyyaml==6.0.1 +toml==0.10.2 + +# Работа с геоданными +geopy==2.4.1 + +# Интеграция с мессенджерами +aiogram==3.17.0 +aiohttp==3.11.11 + +# Планировщик задач +pycron==3.1.2 + +# Мониторинг и логирование +prometheus-client==0.20.0 +sentry-sdk==2.12.1 +loguru==0.7.2 +structlog==24.1.0 +python-json-logger==2.0.7 + +# Тестирование +pytest==8.0.2 +pytest-asyncio==0.23.5 +pytest-cov==5.0.0 +httpx==0.28.1 # Для тестирования FastAPI + +# Утилиты +python-dotenv==1.0.0 +setuptools==75.8.0 +jinja2==3.1.3 # Для шаблонов (если используется) +pillow==11.1.0 # Для обработки изображений diff --git a/backend/requirements.txt b/backend/requirements.txt index d8a6dca..45aaebb 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,7 +5,6 @@ aiohttp==3.11.11 aiosignal==1.3.2 amqp==5.3.1 annotated-types==0.6.0 -anthropic==0.45.2 anyio==4.2.0 argcomplete==1.10.3 asyncpg==0.30.0 @@ -27,7 +26,6 @@ extract-msg==0.28.7 fastapi==0.109.0 frozenlist==1.5.0 greenlet==3.0.3 -groq==0.16.0 h11==0.14.0 httpcore==1.0.7 httpx==0.28.1 @@ -43,7 +41,6 @@ multidict==6.1.0 multivolumefile==0.2.3 numpy==2.2.2 olefile==0.47 -openai==1.61.0 openpyxl==3.1.5 outcome==1.3.0.post0 packaging==24.2 diff --git a/docker-compose.yml b/docker-compose.yml index d53d20e..0310f35 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,101 +1,37 @@ -version: '3.8' +version: '3.8' # Compose Specification v3.8 + services: - backend: + fastapi: build: - context: . - dockerfile: Dockerfile.backend - container_name: dressed-for-success-backend - hostname: backend + context: ./backend # директория с Dockerfile FastAPI + volumes: + - ./backend:/app # монтируем код для live-reload + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload # hot-reload ports: - "8000:8000" - environment: - - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/shop_db - - DEBUG=0 - - SECRET_KEY=supersecretkey - - UPLOAD_DIRECTORY=/app/uploads - - MEILISEARCH_URL=http://meilisearch:7700 - - MEILISEARCH_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM - depends_on: - postgres: - condition: service_healthy - meilisearch: - condition: service_healthy - volumes: - - ./backend/uploads:/app/uploads networks: - app_network: - aliases: - - backend - dns_search: . - restart: always - healthcheck: - test: ["CMD", "curl", "--fail", "http://localhost:8000/" ] - interval: 10s - timeout: 5s - retries: 3 - start_period: 15s + - app-network - frontend: - build: - context: . - dockerfile: Dockerfile.frontend - container_name: dressed-for-success-frontend - hostname: frontend - expose: - - "3000" - environment: - - NEXT_PUBLIC_API_URL=http://0.0.0.0:8000/api - - NEXT_PUBLIC_BASE_URL=http://0.0.0.0:8000 - - NODE_ENV=production - depends_on: - backend: - condition: service_healthy + php: + image: php:8.2-apache # официальный PHP Apache образ + volumes: + - ./php:/var/www/html + ports: + - "8081:80" networks: - app_network: - aliases: - - frontend - dns_search: . - restart: always + - app-network nginx: - image: nginx:alpine - container_name: dressed-for-success-nginx + image: nginx:stable + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # монтируем конфиг NGINX ports: - "80:80" - # - "443:443" # Раскомментируйте для HTTPS - volumes: - - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - - ./backend/uploads:/app/uploads:ro depends_on: - - frontend - - backend + - fastapi + - php networks: - - app_network - restart: always - - postgres: - image: postgres:15 - container_name: dressed-for-success-db - hostname: postgres - environment: - POSTGRES_DB: shop_db - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - ports: - - "5434:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 5s - timeout: 5s - retries: 5 - networks: - app_network: - aliases: - - postgres - dns_search: . - restart: always + - app-network meilisearch: image: getmeili/meilisearch:latest @@ -115,22 +51,16 @@ services: timeout: 5s retries: 3 start_period: 15s + restart: always networks: - app_network: + app-network: aliases: - meilisearch dns_search: . - restart: always - - -networks: - app_network: - driver: bridge volumes: - postgres_data: - driver: local - backend_uploads: - driver: local meilisearch_data: - driver: local \ No newline at end of file + +networks: + app-network: # общая сеть для контейнеров + driver: bridge diff --git a/frontend/.DS_Store b/frontend/.DS_Store index b10d16a5cb36447b701c906f8d113013ca0c84d1..e42401980bbd93060f795597840abe4c4ce960e8 100644 GIT binary patch delta 649 zcmZp1XmOa}jIU^hRb_GBIbDHdJzW9E~!1fwlo14%hASQ9Hl217m&W-}C{7{>6t3Mh{xo15?Al9ZF51hR&G0?Rixlo zpP%LM!{h`J znaKenJS-9cmK%T^4YuV+bX2BZo7^X&1!L|L(PEaBb=dq`gqx9BgRy1uPti2S1(OrR z4koi)2YaxDp^~A1A(f#RIS7&i9ke$?oW{z4qyphQ>?%;4$e^&Xa5Cd&c8PB+lMTfb NChLkB(bMV)i~xc(u&V$7 delta 895 zcmZp1XmOa}&uF|cU^hRb@njwWDHg2_PSTUL1fiSr(5WF6V`$;*9mJe%VclZd|Q~Cky*W8cJfP+G)9HV0ip*3m~ViMFJY); wC;%qCVq`b&*1h1@a2^^5NGcF - + Оформить заказ @@ -432,7 +432,7 @@ export default function CartPage() { disabled={cart.items.length === 0} asChild > - + Оформить заказ diff --git a/frontend/app/(main)/catalog/[slug]/page.tsx b/frontend/app/(main)/catalog/[slug]/page.tsx index 533e348..0203c24 100644 --- a/frontend/app/(main)/catalog/[slug]/page.tsx +++ b/frontend/app/(main)/catalog/[slug]/page.tsx @@ -12,11 +12,12 @@ import { ImageSlider } from "@/components/product/ImageSlider" import { ProductDetails as ProductDetailsComponent } from "@/components/product/ProductDetails" import { Badge } from "@/components/ui/badge" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { motion } from "framer-motion" +import { motion, AnimatePresence } from "framer-motion" import { useInView } from "react-intersection-observer" import { Button } from "@/components/ui/button" import { useCart } from "@/hooks/useCart" import { toast } from "sonner" +import { useWishlist } from "@/hooks/use-wishlist" interface ProductPageProps { params: { @@ -32,18 +33,19 @@ export default function ProductPage({ params }: ProductPageProps) { const [imageRef, imageInView] = useInView({ triggerOnce: true, threshold: 0.1 }) const [detailsRef, detailsInView] = useInView({ triggerOnce: true, threshold: 0.1 }) const { addToCart, loading: cartLoading } = useCart() + const { toggleItem, isInWishlist } = useWishlist(); + + // layout и хлебные крошки всегда видимы + // основной контент fade-in после загрузки - // Загрузка товара при монтировании компонента useEffect(() => { const fetchProduct = async () => { try { setLoading(true) const productData = await catalogService.getProductBySlug(params.slug) - if (!productData) { return notFound() } - setProduct(productData) } catch (err) { console.error("Ошибка при загрузке товара:", err) @@ -52,34 +54,13 @@ export default function ProductPage({ params }: ProductPageProps) { setLoading(false) } } - fetchProduct() }, [params.slug]) - // Если все еще загружается, показываем скелетон - if (loading) { - return - } - - // Если произошла ошибка, показываем сообщение - if (error || !product) { - return ( -
-
-

Товар не найден

-

К сожалению, запрашиваемый товар не найден или произошла ошибка при загрузке данных.

- - - -
-
- ) - } - return (
- {/* Навигация */} + {/* Навигация и хлебные крошки */} Вернуться в каталог - {/* Хлебные крошки */} - - Главная - + Главная - - Каталог - - {product.category_name && ( + Каталог + {product?.category_name && ( <> )} - {product.name} + {product?.name || ''}
- {/* Основной контент товара */}
- {/* Блок изображений */} - - }> -
-
- {/* Используем проверку времени создания для выявления новинок (товары, созданные в течение последних 30 дней) */} - {new Date(product.created_at).getTime() > Date.now() - 30 * 24 * 60 * 60 * 1000 && ( - - Новинка - - )} - {product.discount_price && ( - - -{Math.round((1 - product.discount_price / product.price) * 100)}% - - )} -
- - {/* Кнопка добавления в избранное */} -
- -
- - -
-
-
- - {/* Блок информации о товаре */} - -
- {/* Категория и название */} - - {product.category_name && ( -
- {product.category_name} + {/* Блок изображений и инфо */} + + {loading ? ( + + + + ) : error || !product ? ( + +
+
+

Товар не найден

+

К сожалению, запрашиваемый товар не найден или произошла ошибка при загрузке данных.

+ + +
- )} - -

{product.name}

- - {/* Цена */} -
- {product.discount_price ? ( - <> - - {formatPrice(product.discount_price)} - - - {formatPrice(product.price)} - - - ) : ( - - {formatPrice(product.price)} - - )}
- - - - {/* Компонент выбора размера и количества */} - - - - - {/* Описание товара */} - - - - - Описание - - - Уход - - - - - - {product.description ? ( - typeof product.description === 'string' ? ( -
- ) : ( -

Описание недоступно

- ) - ) : ( -

Описание отсутствует

- )} - - - - {product.care_instructions ? ( - typeof product.care_instructions === 'string' ? ( -
- ) : ( -

Инструкции по уходу недоступны

- ) - ) : ( -

Инструкции по уходу отсутствуют

- )} - - - - - - {/* Информация о доставке и возврате */} - -
-
-
- + ) : ( + +
+ {/* Блок изображений */} +
+
+
+ {product && new Date(product.created_at).getTime() > Date.now() - 30 * 24 * 60 * 60 * 1000 && ( + Новинка + )} + {product?.discount_price && ( + + -{Math.round((1 - (product.discount_price! / product.price)) * 100)}% + + )} +
+ {/* Кнопка добавления в избранное */} +
+ {product && ( + + )} +
+
-

Доставка

-

- Доставка по всей России 1-3 рабочих дня -

-
- -
-
-
- + {/* Блок информации о товаре */} +
+
+ {product?.category_name && ( +
+ {product.category_name} +
+ )} +

{product?.name}

+ {/* Цена */} +
+ {product.discount_price ? ( + <> + + {formatPrice(product.discount_price)} + + + {formatPrice(product.price)} + + + ) : ( + + {formatPrice(product.price)} + + )} +
+ + {/* Компонент выбора размера и количества */} + + + {/* Описание товара */} + + + + + Описание + + + Уход + + + + + + {product.description ? ( + typeof product.description === 'string' ? ( +
+ ) : ( +

Описание недоступно

+ ) + ) : ( +

Описание отсутствует

+ )} + + + + {product.care_instructions ? ( + typeof product.care_instructions === 'string' ? ( +
+ ) : ( +

Инструкции по уходу недоступны

+ ) + ) : ( +

Инструкции по уходу отсутствуют

+ )} + + + + + {/* Информация о доставке и возврате */} + +
+
+
+ +
+

Доставка

+
+

+ Доставка по всей России 1-3 рабочих дня +

+
+ +
+
+
+ +
+

Возврат

+
+

+ Бесплатный возврат в течение 14 дней +

+
+
-

Возврат

-

- Бесплатный возврат в течение 14 дней -

-
- + )} +
diff --git a/frontend/app/(main)/catalog/page.tsx b/frontend/app/(main)/catalog/page.tsx index 15f1c07..5b4a0a8 100644 --- a/frontend/app/(main)/catalog/page.tsx +++ b/frontend/app/(main)/catalog/page.tsx @@ -75,6 +75,9 @@ export default function CatalogPage({ searchParams }: { searchParams?: { [key: s const [isLoadingProducts, setIsLoadingProducts] = useState(false) const [productsError, setProductsError] = useState(null) + // Сохраняем последние успешные продукты для плавного UX + const [lastProducts, setLastProducts] = useState([]) + // Получаем данные из хука const { categories, collections, sizes, loading, error } = catalogData; @@ -178,21 +181,17 @@ export default function CatalogPage({ searchParams }: { searchParams?: { [key: s try { setIsLoadingProducts(true); setProductsError(null); - // Получаем продукты через кэширующий сервис const response = await catalogData.fetchProducts(queryParams); - if (!response || !response.products) { setProductsError("Товары не найдены"); setProducts([]); setTotalProducts(0); return; } - - // Фильтрация по размерам теперь выполняется на бэкенде setProducts(response.products as ExtendedProduct[]); setTotalProducts(response.total); - + setLastProducts(response.products as ExtendedProduct[]); // сохраняем последние успешные } catch (err) { console.error("Ошибка при загрузке продуктов:", err); setProductsError("Не удалось загрузить продукты"); @@ -200,9 +199,7 @@ export default function CatalogPage({ searchParams }: { searchParams?: { [key: s setIsLoadingProducts(false); } }; - loadProducts(); - // Зависим только от параметров запроса и самой функции fetchProducts }, [queryParams, catalogData.fetchProducts]); // Обработчик выбора категории @@ -452,8 +449,10 @@ export default function CatalogPage({ searchParams }: { searchParams?: { [key: s // Компонент сетки товаров const ProductGrid = ({ products, loading }: { products: ExtendedProduct[], loading: boolean }) => { - if (loading) { - // Отображаем скелетон для загрузки + const showProducts = loading && lastProducts.length > 0 ? lastProducts : products; + + // Если идет загрузка (или это первый рендер), всегда показываем только скелетон + if (loading || (products.length === 0 && lastProducts.length === 0)) { return (
{Array.from({ length: 6 }).map((_, i) => ( @@ -469,10 +468,11 @@ export default function CatalogPage({ searchParams }: { searchParams?: { [key: s
))} - ) + ); } - if (products.length === 0) { + // Сообщение 'Товары не найдены' только если загрузка завершена и был хотя бы один успешный запрос + if (!loading && products.length === 0 && lastProducts.length > 0) { return (
@@ -488,19 +488,27 @@ export default function CatalogPage({ searchParams }: { searchParams?: { [key: s
- ) + ); } + // Отображение товаров return (
- {products.map((product) => ( - - ))} + + {showProducts.map((product) => ( + + + + ))} +
- ) + ); } return ( @@ -577,9 +585,7 @@ export default function CatalogPage({ searchParams }: { searchParams?: { [key: s {/* Основной контент с товарами */}
- {isLoadingProducts ? ( - - ) : productsError ? ( + {productsError ? (

Ошибка при загрузке товаров

{productsError}

@@ -587,20 +593,9 @@ export default function CatalogPage({ searchParams }: { searchParams?: { [key: s Попробовать снова
- ) : products.length === 0 ? ( -
-

Товары не найдены

-

- Попробуйте изменить параметры фильтрации или поискать что-то другое. -

- -
) : ( <> - {/* Пагинация */} {totalProducts > 12 && (
diff --git a/frontend/app/(main)/checkout/page.tsx b/frontend/app/(main)/checkout/page.tsx index 7dec4d9..99079d2 100644 --- a/frontend/app/(main)/checkout/page.tsx +++ b/frontend/app/(main)/checkout/page.tsx @@ -13,6 +13,7 @@ import OrderSummary from "@/components/checkout/order-summary" import { Separator } from "@/components/ui/separator" import OrderComment from "@/components/checkout/order-comment" import { orderService } from "@/lib/order-service" +import { OrderCreate, Address } from "@/types/order" import { useToast } from "@/components/ui/use-toast" import { Loader2 } from "lucide-react" @@ -24,10 +25,10 @@ export default function CheckoutPage() { const { cart, loading, clearCart } = useCart() const { toast } = useToast() const router = useRouter() - + // Состояния для обработки заказа const [isSubmitting, setIsSubmitting] = useState(false) - + // Состояния для форм и выбранных опций const [userInfo, setUserInfo] = useState({ firstName: "", @@ -35,47 +36,82 @@ export default function CheckoutPage() { email: "", phone: "" }) - + const [deliveryMethod, setDeliveryMethod] = useState("cdek") - - const [address, setAddress] = useState({ + + const [address, setAddress] = useState
({ city: "Новокузнецк", // По умолчанию street: "", house: "", apartment: "", postalCode: "" }) - + const [paymentMethod, setPaymentMethod] = useState("sbp") - + const [orderComment, setOrderComment] = useState("") - + + // Собираем массив goods для СДЭК (TODO: заменить на реальные размеры/вес из товара) + const goods = cart.items.map(item => ({ + length: 20, // TODO: заменить на реальные значения + width: 20, + height: 20, + weight: 2, // кг + })); + // Проверка валидности заполнения формы const isFormValid = () => { - const isUserInfoValid = - userInfo.firstName && - userInfo.email && + // Проверка информации о пользователе + const isUserInfoValid = + userInfo.firstName && + userInfo.email && userInfo.phone - - const isAddressValid = - address.city && - address.street && - address.house - - return isUserInfoValid && isAddressValid + + // Проверка адреса в зависимости от способа доставки + let isAddressValid = false; + + if (deliveryMethod === "cdek") { + // Для CDEK нужен выбранный пункт выдачи + isAddressValid = !!address.city && !!address.cdekPvz; + + // Добавляем логирование для отладки + console.log("Проверка валидности формы для CDEK:", { + city: address.city, + cdekPvz: !!address.cdekPvz, + isAddressValid + }); + } else { + // Для курьерской доставки нужен адрес + isAddressValid = + !!address.city && + !!address.street && + !!address.house; + } + + return isUserInfoValid && isAddressValid; } - + // Обработчик оформления заказа const handleSubmitOrder = async () => { + // Проверка валидности формы if (!isFormValid()) { - toast({ - variant: "destructive", - title: "Форма не заполнена", - description: "Пожалуйста, заполните все обязательные поля" - }) + // Проверяем, что именно не заполнено + if (deliveryMethod === "cdek" && !address.cdekPvz) { + toast({ + variant: "destructive", + title: "Не выбран пункт выдачи СДЭК", + description: "Пожалуйста, нажмите кнопку 'Выбрать пункт выдачи' и выберите пункт выдачи СДЭК" + }) + } else { + toast({ + variant: "destructive", + title: "Форма не заполнена", + description: "Пожалуйста, заполните все обязательные поля" + }) + } return } - + if (!cart.items || cart.items.length === 0) { toast({ variant: "destructive", @@ -84,12 +120,12 @@ export default function CheckoutPage() { }) return } - + try { setIsSubmitting(true) - + // Подготавливаем данные для заказа - const orderData = { + const orderData: OrderCreate = { userInfo, items: cart.items, address, @@ -97,7 +133,10 @@ export default function CheckoutPage() { paymentMethod, comment: orderComment } - + + // Вызываем отладочную функцию для анализа структуры заказа + orderService.debugOrderStructure(orderData) + // Сохраняем состояние корзины для показа на странице успешного оформления try { // Создаем копию корзины @@ -106,13 +145,13 @@ export default function CheckoutPage() { } catch (error) { console.error("Ошибка при сохранении состояния корзины:", error); } - + // Создаем заказ const order = await orderService.createOrder(orderData) - + // Очищаем корзину await clearCart() - + // Перенаправляем на страницу успешного оформления router.push(`/checkout/success?order_id=${order.orderId}&total=${order.total}&email=${encodeURIComponent(userInfo.email)}`) } catch (error) { @@ -123,7 +162,7 @@ export default function CheckoutPage() { } catch (error) { console.error("Ошибка при очистке данных заказа:", error); } - + toast({ variant: "destructive", title: "Ошибка", @@ -133,7 +172,7 @@ export default function CheckoutPage() { setIsSubmitting(false) } } - + // Проверка наличия товаров в корзине if (!loading && (!cart.items || cart.items.length === 0)) { return ( @@ -141,7 +180,7 @@ export default function CheckoutPage() {

Оформление заказа

Ваша корзина пуста

- ) : ( -
+
-

- ТОВАРЫ В ИЗБРАННОМ ({wishlistItems.length}) +

+ Товары в избранном ({products.length})

-
- {wishlistItems.map((item) => ( - -
-
- {item.name} -
- - - - {!item.inStock && ( -
- -

НЕТ В НАЛИЧИИ

-

Сообщить, когда появится

-
- )} -
- -
- -

- {item.name} -

- - -
- {item.price.toLocaleString()} ₽ - {item.oldPrice && ( - - {item.oldPrice.toLocaleString()} ₽ - - )} -
- -
- {item.inStock ? ( - <> - - - - ) : ( - - )} -
-
-
+ {products.map((product) => ( +
+ + +
))}
-
+ )} -
- - - {/* Recommended Products */} -
-
-

РЕКОМЕНДУЕМ ВАМ

+ + {/* Рекомендации */} +
+

Рекомендуем вам

- {[ - { - id: 5, - name: "ЮБКА МИДИ ПЛИССЕ", - price: 3490, - image: "/placeholder.svg?height=600&width=400", - }, - { - id: 6, - name: "ПАЛЬТО ИЗ ШЕРСТИ", - price: 12990, - image: "/placeholder.svg?height=600&width=400", - }, - { - id: 7, - name: "ДЖЕМПЕР ИЗ КАШЕМИРА", - price: 7990, - oldPrice: 9990, - image: "/placeholder.svg?height=600&width=400", - }, - { - id: 8, - name: "РУБАШКА ОВЕРСАЙЗ", - price: 4490, - image: "/placeholder.svg?height=600&width=400", - }, - ].map((item) => ( -
-
- {item.name} - - -
- -
- -

- {item.name} -

- - -
- {item.price.toLocaleString()} ₽ - {item.oldPrice && ( - - {item.oldPrice.toLocaleString()} ₽ - - )} -
- - -
-
+ {recommended.map((item) => ( + ))}
-
-
+ +
) } diff --git a/frontend/app/.DS_Store b/frontend/app/.DS_Store index 411dca8400036bf49cb880b402690e463ca3c074..6d0fc2fa1ee8cd10ac167049d90679783d61f833 100644 GIT binary patch delta 94 zcmZoMXffEJ$;8ZJA30fvNrr8raMIVbYm@z$no{9Ox=8eo@EE5~}HnVg5; +import { Category } from '@/lib/catalog-admin'; +import { useAdminMutation } from '@/hooks/useAdminQuery'; +import { cacheKeys } from '@/lib/api-cache'; +import useCategoriesCache from '@/hooks/useCategoriesCache'; -// Компонент для отображения категории в виде дерева -const CategoryItem = ({ - category, - level = 0, - onEdit, - onDelete, - onAddSubcategory, - categories, -}: { - category: Category; - level?: number; - onEdit: (category: Category) => void; - onDelete: (categoryId: number) => void; - onAddSubcategory: (parentId: number) => void; - categories: Category[]; -}) => { - const [isExpanded, setIsExpanded] = useState(false); - const hasSubcategories = category.subcategories && category.subcategories.length > 0; - return ( -
-
- {hasSubcategories ? ( - - ) : ( -
- )} -
{category.name}
-
- - - -
-
- {isExpanded && category.subcategories && category.subcategories.length > 0 && ( -
- {category.subcategories.map((subcategory) => ( - - ))} -
- )} -
- ); -}; -// Компонент диалогового окна для редактирования категории -const EditCategoryDialog = ({ - isOpen, - onClose, - category, - onSave, - categories, - mode, - parentId, -}: { - isOpen: boolean; - onClose: () => void; - category: Category | null; - onSave: (data: CategoryFormValues) => void; - categories: Category[]; - mode: 'edit' | 'create'; - parentId?: number | null; -}) => { - const form = useForm({ - resolver: zodResolver(categoryFormSchema), - defaultValues: category - ? { - name: category.name, - slug: category.slug || '', - description: category.description || '', - parent_id: category.parent_id, - is_active: category.is_active, - } - : { - name: '', - slug: '', - description: '', - parent_id: parentId || null, - is_active: true, - }, - }); - - useEffect(() => { - if (isOpen) { - form.reset( - category - ? { - name: category.name, - slug: category.slug || '', - description: category.description || '', - parent_id: category.parent_id, - is_active: category.is_active, - } - : { - name: '', - slug: '', - description: '', - parent_id: parentId || null, - is_active: true, - } - ); - } - }, [category, form, isOpen, parentId]); - - const handleSubmit = (data: CategoryFormValues) => { - onSave(data); - }; - - // Функция для получения плоского списка категорий для выбора родителя - const getSelectableCategories = ( - categories: Category[], - excludeId: number | null = null - ): { id: number; name: string; level: number }[] => { - const result: { id: number; name: string; level: number }[] = []; - - const traverse = (cats: Category[], level = 0) => { - cats.forEach((cat) => { - if (excludeId === null || cat.id !== excludeId) { - result.push({ id: cat.id, name: cat.name, level }); - if (cat.subcategories && cat.subcategories.length > 0) { - traverse(cat.subcategories, level + 1); - } - } - }); - }; - - traverse(categories); - return result; - }; - - const selectableCategories = getSelectableCategories( - categories, - mode === 'edit' ? category?.id : null - ); - - return ( - !open && onClose()}> - - - - {mode === 'edit' ? 'Редактирование категории' : 'Создание категории'} - - - {mode === 'edit' - ? 'Отредактируйте информацию о категории' - : 'Создайте новую категорию'} - - -
- - ( - - Название категории - - - - - - )} - /> - ( - - Slug (для URL) - - - - - Используется в URL. Только латинские буквы, цифры и дефисы. - - - - )} - /> - ( - - Описание - -