diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4bee93..4ab5dc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [main, warehouse-dev] pull_request: defaults: diff --git a/.gitignore b/.gitignore index 9f11b75..46a1b26 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .idea/ +dev-stderr.log +dev-stdout.log diff --git a/resolution-frontend/.env.example b/resolution-frontend/.env.example index 7f7f4cc..ef1645c 100644 --- a/resolution-frontend/.env.example +++ b/resolution-frontend/.env.example @@ -12,6 +12,29 @@ AIRTABLE_BASE_ID=your_base_id AIRTABLE_TABLE_ID=your_table_id AIRTABLE_YSWS_TABLE_ID=your_ysws_project_submission_table_id +# Canada Post API (for shipping rate estimates) +CP_API_USERNAME=your_cp_api_username +CP_API_PASSWORD=your_cp_api_password +CP_CUSTOMER_NUMBER=your_cp_customer_number +CP_CONTRACT_ID= # optional +CP_ORIGIN_POSTAL_CODE=A1A1A1 +CP_ENVIRONMENT=development # or "production" +ZONOS_ACCOUNT_KEY= # Zonos Verified Account key for US duty prepayment +ZONOS_CREDENTIAL_TOKEN= # Zonos API credential token for landed cost calculations + +# Chit Chats API (alternative shipping provider) +CHITCHATS_CLIENT_ID=your_chitchats_client_id +CHITCHATS_ACCESS_TOKEN=your_chitchats_access_token + +# Hack Club CDN (for warehouse image uploads) +HACK_CLUB_CDN_API_KEY=sk_cdn_your_key_here + +# HCB OAuth (for warehouse order billing — auto-refreshes every 30 min) +HCB_CLIENT_ID=your_hcb_oauth_client_id +HCB_CLIENT_SECRET=your_hcb_oauth_client_secret +HCB_ACCESS_TOKEN=your_hcb_access_token +HCB_REFRESH_TOKEN=your_hcb_refresh_token + # Season Configuration # Only SEASON_STARTS is required - everything else is auto-calculated SEASON_STARTS="2026-01-01" diff --git a/resolution-frontend/.gitignore b/resolution-frontend/.gitignore index fbd6ae0..9cf95f4 100644 --- a/resolution-frontend/.gitignore +++ b/resolution-frontend/.gitignore @@ -24,6 +24,10 @@ vite.config.ts.timestamp-* /docs +# Coverage +/coverage + # Prisma *.db *.db-journal + diff --git a/resolution-frontend/Dockerfile b/resolution-frontend/Dockerfile index 69351ce..b7ea456 100644 --- a/resolution-frontend/Dockerfile +++ b/resolution-frontend/Dockerfile @@ -39,11 +39,25 @@ COPY --from=builder /app/tsconfig.json ./tsconfig.json # Install drizzle-kit locally so we don't rely on npx fetching it at runtime RUN npm install --no-save drizzle-kit +# Copy drizzle files needed for push +COPY --from=builder /app/drizzle ./drizzle +COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts +COPY --from=builder /app/src/lib/server/db/schema.ts ./src/lib/server/db/schema.ts + +# Install drizzle-kit for runtime schema push (pruned as devDependency) +RUN npm install drizzle-kit@0.31.8 + +# Copy entrypoint script +COPY entrypoint.sh ./entrypoint.sh +RUN chmod +x ./entrypoint.sh + # Expose the port the app runs on (SvelteKit node adapter starts on 3000 by default) EXPOSE 3000 # Set Node environment to production ENV NODE_ENV=production +# Increase body size limit for image uploads (default 512K is too small) +ENV BODY_SIZE_LIMIT=10M -# Push DB schema and start the application -CMD ["sh", "-c", "npx drizzle-kit push && node build"] +# Start the application with migrations +CMD ["./entrypoint.sh"] diff --git a/resolution-frontend/bun.lock b/resolution-frontend/bun.lock index b3ba8d7..8792dc6 100644 --- a/resolution-frontend/bun.lock +++ b/resolution-frontend/bun.lock @@ -5,21 +5,112 @@ "": { "name": "revolution-frontend", "dependencies": { - "dompurify": "^3.3.1", + "@lucia-auth/adapter-drizzle": "^1.1.0", + "@monaco-editor/loader": "^1.7.0", + "@paralleldrive/cuid2": "^3.3.0", + "@tsparticles/fireworks": "^3.9.1", + "@types/canvas-confetti": "^1.9.0", + "@types/uuid": "^10.0.0", + "airtable": "^0.12.2", + "bcrypt": "^6.0.0", + "canvas-confetti": "^1.9.4", + "dompurify": "^3.3.3", + "dotenv": "^16.4.0", + "drizzle-orm": "^0.45.1", + "jsonwebtoken": "^9.0.3", + "lucia": "^3.2.2", + "monaco-editor": "^0.55.1", + "oslo": "^1.2.1", + "papaparse": "^5.5.3", + "pdf-lib": "^1.17.1", + "pg": "^8.13.0", + "simple-oauth2": "^5.1.0", + "svelte-marked": "^0.8.0", + "tsparticles": "^3.9.1", + "tsparticles-preset-fireworks": "^2.12.0", + "uuid": "^13.0.0", + "xml2js": "^0.6.2", + "zod": "^4.3.5", }, "devDependencies": { "@sveltejs/adapter-auto": "^7.0.0", - "@sveltejs/kit": "^2.49.1", + "@sveltejs/adapter-node": "^5.5.1", + "@sveltejs/kit": "^2.49.5", "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/svelte": "^5.3.1", + "@types/bcrypt": "^6.0.0", "@types/dompurify": "^3.2.0", - "svelte": "^5.45.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/papaparse": "^5.5.2", + "@types/pg": "^8.16.0", + "@types/simple-oauth2": "^5.0.8", + "@types/xml2js": "^0.4.14", + "@vitest/coverage-v8": "^4.0.18", + "drizzle-kit": "^0.31.8", + "jsdom": "^28.1.0", + "svelte": "^5.46.4", "svelte-check": "^4.3.4", "typescript": "^5.9.3", "vite": "^7.2.6", + "vitest": "^4.0.18", }, }, }, + "overrides": { + "cookie": ">=0.7.0", + "dompurify": "^3.3.3", + "esbuild": ">=0.25.0", + }, "packages": { + "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="], + + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.9", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-zd9c/Wdso6v1U7v6w3i/hbAr4K7NaSHImdpvmLt+Y9ea5BhilnIGNkfhOJ7FEIuPipAnE9tZeDOll05WDT0kgg=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.8.1", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.6" } }, "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + + "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="], + + "@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.2", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.2", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@emnapi/core": ["@emnapi/core@0.45.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw=="], + + "@emnapi/runtime": ["@emnapi/runtime@0.45.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], @@ -72,6 +163,18 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="], + + "@hapi/boom": ["@hapi/boom@10.0.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA=="], + + "@hapi/bourne": ["@hapi/bourne@3.0.0", "", {}, "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w=="], + + "@hapi/hoek": ["@hapi/hoek@11.0.7", "", {}, "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ=="], + + "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], + + "@hapi/wreck": ["@hapi/wreck@18.1.0", "", { "dependencies": { "@hapi/boom": "^10.0.1", "@hapi/bourne": "^3.0.0", "@hapi/hoek": "^11.0.2" } }, "sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -82,51 +185,151 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@lucia-auth/adapter-drizzle": ["@lucia-auth/adapter-drizzle@1.1.0", "", { "peerDependencies": { "drizzle-orm": ">= 0.29 <1", "lucia": "3.x" } }, "sha512-iCTnZWvfI5lLZOdUHZYiXA1jaspIFEeo2extLxQ3DjP3uOVys7IPwBi7zezLIRu9dhro4H4Kji+7gSYyjcef2A=="], + + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], + + "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + + "@node-rs/argon2": ["@node-rs/argon2@1.7.0", "", { "optionalDependencies": { "@node-rs/argon2-android-arm-eabi": "1.7.0", "@node-rs/argon2-android-arm64": "1.7.0", "@node-rs/argon2-darwin-arm64": "1.7.0", "@node-rs/argon2-darwin-x64": "1.7.0", "@node-rs/argon2-freebsd-x64": "1.7.0", "@node-rs/argon2-linux-arm-gnueabihf": "1.7.0", "@node-rs/argon2-linux-arm64-gnu": "1.7.0", "@node-rs/argon2-linux-arm64-musl": "1.7.0", "@node-rs/argon2-linux-x64-gnu": "1.7.0", "@node-rs/argon2-linux-x64-musl": "1.7.0", "@node-rs/argon2-wasm32-wasi": "1.7.0", "@node-rs/argon2-win32-arm64-msvc": "1.7.0", "@node-rs/argon2-win32-ia32-msvc": "1.7.0", "@node-rs/argon2-win32-x64-msvc": "1.7.0" } }, "sha512-zfULc+/tmcWcxn+nHkbyY8vP3+MpEqKORbszt4UkpqZgBgDAAIYvuDN/zukfTgdmo6tmJKKVfzigZOPk4LlIog=="], + + "@node-rs/argon2-android-arm-eabi": ["@node-rs/argon2-android-arm-eabi@1.7.0", "", { "os": "android", "cpu": "arm" }, "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg=="], + + "@node-rs/argon2-android-arm64": ["@node-rs/argon2-android-arm64@1.7.0", "", { "os": "android", "cpu": "arm64" }, "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A=="], + + "@node-rs/argon2-darwin-arm64": ["@node-rs/argon2-darwin-arm64@1.7.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZIz4L6HGOB9U1kW23g+m7anGNuTZ0RuTw0vNp3o+2DWpb8u8rODq6A8tH4JRL79S+Co/Nq608m9uackN2pe0Rw=="], + + "@node-rs/argon2-darwin-x64": ["@node-rs/argon2-darwin-x64@1.7.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw=="], + + "@node-rs/argon2-freebsd-x64": ["@node-rs/argon2-freebsd-x64@1.7.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA=="], + + "@node-rs/argon2-linux-arm-gnueabihf": ["@node-rs/argon2-linux-arm-gnueabihf@1.7.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg=="], + + "@node-rs/argon2-linux-arm64-gnu": ["@node-rs/argon2-linux-arm64-gnu@1.7.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g=="], + + "@node-rs/argon2-linux-arm64-musl": ["@node-rs/argon2-linux-arm64-musl@1.7.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A=="], + + "@node-rs/argon2-linux-x64-gnu": ["@node-rs/argon2-linux-x64-gnu@1.7.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EmgqZOlf4Jurk/szW1iTsVISx25bKksVC5uttJDUloTgsAgIGReCpUUO1R24pBhu9ESJa47iv8NSf3yAfGv6jQ=="], + + "@node-rs/argon2-linux-x64-musl": ["@node-rs/argon2-linux-x64-musl@1.7.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA=="], + + "@node-rs/argon2-wasm32-wasi": ["@node-rs/argon2-wasm32-wasi@1.7.0", "", { "dependencies": { "@emnapi/core": "^0.45.0", "@emnapi/runtime": "^0.45.0", "@tybys/wasm-util": "^0.8.1", "memfs-browser": "^3.4.13000" }, "cpu": "none" }, "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w=="], + + "@node-rs/argon2-win32-arm64-msvc": ["@node-rs/argon2-win32-arm64-msvc@1.7.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA=="], + + "@node-rs/argon2-win32-ia32-msvc": ["@node-rs/argon2-win32-ia32-msvc@1.7.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg=="], + + "@node-rs/argon2-win32-x64-msvc": ["@node-rs/argon2-win32-x64-msvc@1.7.0", "", { "os": "win32", "cpu": "x64" }, "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q=="], + + "@node-rs/bcrypt": ["@node-rs/bcrypt@1.9.0", "", { "optionalDependencies": { "@node-rs/bcrypt-android-arm-eabi": "1.9.0", "@node-rs/bcrypt-android-arm64": "1.9.0", "@node-rs/bcrypt-darwin-arm64": "1.9.0", "@node-rs/bcrypt-darwin-x64": "1.9.0", "@node-rs/bcrypt-freebsd-x64": "1.9.0", "@node-rs/bcrypt-linux-arm-gnueabihf": "1.9.0", "@node-rs/bcrypt-linux-arm64-gnu": "1.9.0", "@node-rs/bcrypt-linux-arm64-musl": "1.9.0", "@node-rs/bcrypt-linux-x64-gnu": "1.9.0", "@node-rs/bcrypt-linux-x64-musl": "1.9.0", "@node-rs/bcrypt-wasm32-wasi": "1.9.0", "@node-rs/bcrypt-win32-arm64-msvc": "1.9.0", "@node-rs/bcrypt-win32-ia32-msvc": "1.9.0", "@node-rs/bcrypt-win32-x64-msvc": "1.9.0" } }, "sha512-u2OlIxW264bFUfvbFqDz9HZKFjwe8FHFtn7T/U8mYjPZ7DWYpbUB+/dkW/QgYfMSfR0ejkyuWaBBe0coW7/7ig=="], + + "@node-rs/bcrypt-android-arm-eabi": ["@node-rs/bcrypt-android-arm-eabi@1.9.0", "", { "os": "android", "cpu": "arm" }, "sha512-nOCFISGtnodGHNiLrG0WYLWr81qQzZKYfmwHc7muUeq+KY0sQXyHOwZk9OuNQAWv/lnntmtbwkwT0QNEmOyLvA=="], + + "@node-rs/bcrypt-android-arm64": ["@node-rs/bcrypt-android-arm64@1.9.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+ZrIAtigVmjYkqZQTThHVlz0+TG6D+GDHWhVKvR2DifjtqJ0i+mb9gjo++hN+fWEQdWNGxKCiBBjwgT4EcXd6A=="], + + "@node-rs/bcrypt-darwin-arm64": ["@node-rs/bcrypt-darwin-arm64@1.9.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CQiS+F9Pa0XozvkXR1g7uXE9QvBOPOplDg0iCCPRYTN9PqA5qYxhwe48G3o+v2UeQceNRrbnEtWuANm7JRqIhw=="], + + "@node-rs/bcrypt-darwin-x64": ["@node-rs/bcrypt-darwin-x64@1.9.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-4pTKGawYd7sNEjdJ7R/R67uwQH1VvwPZ0SSUMmeNHbxD5QlwAPXdDH11q22uzVXsvNFZ6nGQBg8No5OUGpx6Ug=="], + + "@node-rs/bcrypt-freebsd-x64": ["@node-rs/bcrypt-freebsd-x64@1.9.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UmWzySX4BJhT/B8xmTru6iFif3h0Rpx3TqxRLCcbgmH43r7k5/9QuhpiyzpvKGpKHJCFNm4F3rC2wghvw5FCIg=="], + + "@node-rs/bcrypt-linux-arm-gnueabihf": ["@node-rs/bcrypt-linux-arm-gnueabihf@1.9.0", "", { "os": "linux", "cpu": "arm" }, "sha512-8qoX4PgBND2cVwsbajoAWo3NwdfJPEXgpCsZQZURz42oMjbGyhhSYbovBCskGU3EBLoC8RA2B1jFWooeYVn5BA=="], + + "@node-rs/bcrypt-linux-arm64-gnu": ["@node-rs/bcrypt-linux-arm64-gnu@1.9.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TuAC6kx0SbcIA4mSEWPi+OCcDjTQUMl213v5gMNlttF+D4ieIZx6pPDGTaMO6M2PDHTeCG0CBzZl0Lu+9b0c7Q=="], + + "@node-rs/bcrypt-linux-arm64-musl": ["@node-rs/bcrypt-linux-arm64-musl@1.9.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/sIvKDABOI8QOEnLD7hIj02BVaNOuCIWBKvxcJOt8+TuwJ6zmY1UI5kSv9d99WbiHjTp97wtAUbZQwauU4b9ew=="], + + "@node-rs/bcrypt-linux-x64-gnu": ["@node-rs/bcrypt-linux-x64-gnu@1.9.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DyyhDHDsLBsCKz1tZ1hLvUZSc1DK0FU0v52jK6IBQxrj24WscSU9zZe7ie/V9kdmA4Ep57BfpWX8Dsa2JxGdgQ=="], + + "@node-rs/bcrypt-linux-x64-musl": ["@node-rs/bcrypt-linux-x64-musl@1.9.0", "", { "os": "linux", "cpu": "x64" }, "sha512-duIiuqQ+Lew8ASSAYm6ZRqcmfBGWwsi81XLUwz86a2HR7Qv6V4yc3ZAUQovAikhjCsIqe8C11JlAZSK6+PlXYg=="], + + "@node-rs/bcrypt-wasm32-wasi": ["@node-rs/bcrypt-wasm32-wasi@1.9.0", "", { "dependencies": { "@emnapi/core": "^0.45.0", "@emnapi/runtime": "^0.45.0", "@tybys/wasm-util": "^0.8.1", "memfs-browser": "^3.4.13000" }, "cpu": "none" }, "sha512-ylaGmn9Wjwv/D5lxtawttx3H6Uu2WTTR7lWlRHGT6Ga/MB1Vj4OjSGUW8G8zIVnKuXpGbZ92pgHlt4HUpSLctw=="], + + "@node-rs/bcrypt-win32-arm64-msvc": ["@node-rs/bcrypt-win32-arm64-msvc@1.9.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2h86gF7QFyEzODuDFml/Dp1MSJoZjxJ4yyT2Erf4NkwsiA5MqowUhUsorRwZhX6+2CtlGa7orbwi13AKMsYndw=="], + + "@node-rs/bcrypt-win32-ia32-msvc": ["@node-rs/bcrypt-win32-ia32-msvc@1.9.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-kqxalCvhs4FkN0+gWWfa4Bdy2NQAkfiqq/CEf6mNXC13RSV673Ev9V8sRlQyNpCHCNkeXfOT9pgoBdJmMs9muA=="], + + "@node-rs/bcrypt-win32-x64-msvc": ["@node-rs/bcrypt-win32-x64-msvc@1.9.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2y0Tuo6ZAT2Cz8V7DHulSlv1Bip3zbzeXyeur+uR25IRNYXKvI/P99Zl85Fbuu/zzYAZRLLlGTRe6/9IHofe/w=="], + + "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], + + "@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="], + + "@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="], + + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], + + "@paralleldrive/cuid2": ["@paralleldrive/cuid2@3.3.0", "", { "dependencies": { "@noble/hashes": "^2.0.1", "bignumber.js": "^9.3.1", "error-causes": "^3.0.2" }, "bin": { "cuid2": "bin/cuid2.js" } }, "sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw=="], + + "@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="], + + "@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="], + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], + "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg=="], + + "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="], + + "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.54.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ=="], + "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ=="], + "@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], + "@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -134,58 +337,370 @@ "@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw=="], - "@sveltejs/kit": ["@sveltejs/kit@2.49.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ=="], + "@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.4", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.59.0" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.57.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw=="], "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="], "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="], + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/svelte": ["@testing-library/svelte@5.3.1", "", { "dependencies": { "@testing-library/dom": "9.x.x || 10.x.x", "@testing-library/svelte-core": "1.0.0" }, "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", "vite": "*", "vitest": "*" }, "optionalPeers": ["vite", "vitest"] }, "sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w=="], + + "@testing-library/svelte-core": ["@testing-library/svelte-core@1.0.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" } }, "sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ=="], + + "@tsparticles/basic": ["@tsparticles/basic@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1", "@tsparticles/move-base": "3.9.1", "@tsparticles/plugin-hex-color": "3.9.1", "@tsparticles/plugin-hsl-color": "3.9.1", "@tsparticles/plugin-rgb-color": "3.9.1", "@tsparticles/shape-circle": "3.9.1", "@tsparticles/updater-color": "3.9.1", "@tsparticles/updater-opacity": "3.9.1", "@tsparticles/updater-out-modes": "3.9.1", "@tsparticles/updater-size": "3.9.1" } }, "sha512-ijr2dHMx0IQHqhKW3qA8tfwrR2XYbbWYdaJMQuBo2CkwBVIhZ76U+H20Y492j/NXpd1FUnt2aC0l4CEVGVGdeQ=="], + + "@tsparticles/effect-trail": ["@tsparticles/effect-trail@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-6vo7rsI+xta8Cqs9TMW00uoyqddHjAu8qVBKjMicSkVm18A6BxF5Puv0MVgTkikdWa3MMztXOqJI3YmWjpJb0w=="], + + "@tsparticles/engine": ["@tsparticles/engine@3.9.1", "", {}, "sha512-DpdgAhWMZ3Eh2gyxik8FXS6BKZ8vyea+Eu5BC4epsahqTGY9V3JGGJcXC6lRJx6cPMAx1A0FaQAojPF3v6rkmQ=="], + + "@tsparticles/fireworks": ["@tsparticles/fireworks@3.9.1", "", { "dependencies": { "@tsparticles/basic": "3.9.1", "@tsparticles/effect-trail": "3.9.1", "@tsparticles/engine": "3.9.1", "@tsparticles/plugin-emitters": "3.9.1", "@tsparticles/plugin-emitters-shape-square": "3.9.1", "@tsparticles/plugin-sounds": "3.9.1", "@tsparticles/updater-destroy": "3.9.1", "@tsparticles/updater-life": "3.9.1", "@tsparticles/updater-rotate": "3.9.1" } }, "sha512-75zJlQxJEISsRLr4TsQ8kxbZT+kg/uScqA+WVvHHmdA3UdSvBrgdcZPY0QjdmjZxWwab+O1qDRzf9kQfsr4luQ=="], + + "@tsparticles/interaction-external-attract": ["@tsparticles/interaction-external-attract@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-5AJGmhzM9o4AVFV24WH5vSqMBzOXEOzIdGLIr+QJf4fRh9ZK62snsusv/ozKgs2KteRYQx+L7c5V3TqcDy2upg=="], + + "@tsparticles/interaction-external-bounce": ["@tsparticles/interaction-external-bounce@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-bv05+h70UIHOTWeTsTI1AeAmX6R3s8nnY74Ea6p6AbQjERzPYIa0XY19nq/hA7+Nrg+EissP5zgoYYeSphr85A=="], + + "@tsparticles/interaction-external-bubble": ["@tsparticles/interaction-external-bubble@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-tbd8ox/1GPl+zr+KyHQVV1bW88GE7OM6i4zql801YIlCDrl9wgTDdDFGIy9X7/cwTvTrCePhrfvdkUamXIribQ=="], + + "@tsparticles/interaction-external-connect": ["@tsparticles/interaction-external-connect@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-sq8YfUNsIORjXHzzW7/AJQtfi/qDqLnYG2qOSE1WOsog39MD30RzmiOloejOkfNeUdcGUcfsDgpUuL3UhzFUOA=="], + + "@tsparticles/interaction-external-grab": ["@tsparticles/interaction-external-grab@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-QwXza+sMMWDaMiFxd8y2tJwUK6c+nNw554+/9+tEZeTTk2fCbB0IJ7p/TH6ZGWDL0vo2muK54Njv2fEey191ow=="], + + "@tsparticles/interaction-external-pause": ["@tsparticles/interaction-external-pause@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-Gzv4/FeNir0U/tVM9zQCqV1k+IAgaFjDU3T30M1AeAsNGh/rCITV2wnT7TOGFkbcla27m4Yxa+Fuab8+8pzm+g=="], + + "@tsparticles/interaction-external-push": ["@tsparticles/interaction-external-push@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-GvnWF9Qy4YkZdx+WJL2iy9IcgLvzOIu3K7aLYJFsQPaxT8d9TF8WlpoMlWKnJID6H5q4JqQuMRKRyWH8aAKyQw=="], + + "@tsparticles/interaction-external-remove": ["@tsparticles/interaction-external-remove@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-yPThm4UDWejDOWW5Qc8KnnS2EfSo5VFcJUQDWc1+Wcj17xe7vdSoiwwOORM0PmNBzdDpSKQrte/gUnoqaUMwOA=="], + + "@tsparticles/interaction-external-repulse": ["@tsparticles/interaction-external-repulse@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-/LBppXkrMdvLHlEKWC7IykFhzrz+9nebT2fwSSFXK4plEBxDlIwnkDxd3FbVOAbnBvx4+L8+fbrEx+RvC8diAw=="], + + "@tsparticles/interaction-external-slow": ["@tsparticles/interaction-external-slow@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-1ZYIR/udBwA9MdSCfgADsbDXKSFS0FMWuPWz7bm79g3sUxcYkihn+/hDhc6GXvNNR46V1ocJjrj0u6pAynS1KQ=="], + + "@tsparticles/interaction-external-trail": ["@tsparticles/interaction-external-trail@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-Au0v2oiqfKTemI/4bzjD4dUXzIngB5Q2T4nJcMCYpP24uZfwZh5xTjUMH7gyJyyaRTdMl9IJrp8ySjyYbLfeGg=="], + + "@tsparticles/interaction-particles-attract": ["@tsparticles/interaction-particles-attract@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-CYYYowJuGwRLUixQcSU/48PTKM8fCUYThe0hXwQ+yRMLAn053VHzL7NNZzKqEIeEyt5oJoy9KcvubjKWbzMBLQ=="], + + "@tsparticles/interaction-particles-collisions": ["@tsparticles/interaction-particles-collisions@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-ggGyjW/3v1yxvYW1IF1EMT15M6w31y5zfNNUPkqd/IXRNPYvm0Z0ayhp+FKmz70M5p0UxxPIQHTvAv9Jqnuj8w=="], + + "@tsparticles/interaction-particles-links": ["@tsparticles/interaction-particles-links@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-MsLbMjy1vY5M5/hu/oa5OSRZAUz49H3+9EBMTIOThiX+a+vpl3sxc9AqNd9gMsPbM4WJlub8T6VBZdyvzez1Vg=="], + + "@tsparticles/move-base": ["@tsparticles/move-base@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-X4huBS27d8srpxwOxliWPUt+NtCwY+8q/cx1DvQxyqmTA8VFCGpcHNwtqiN+9JicgzOvSuaORVqUgwlsc7h4pQ=="], + + "@tsparticles/move-parallax": ["@tsparticles/move-parallax@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-whlOR0bVeyh6J/hvxf/QM3DqvNnITMiAQ0kro6saqSDItAVqg4pYxBfEsSOKq7EhjxNvfhhqR+pFMhp06zoCVA=="], + + "@tsparticles/plugin-absorbers": ["@tsparticles/plugin-absorbers@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-q9SQllpbPPgw1+euxHPYCFawOVUazQkkwnleiIgpYSiimlCyjIdwGnFPSNe1Sypzqmr2h6oOyX2vkK5ZVNEu8A=="], + + "@tsparticles/plugin-easing-quad": ["@tsparticles/plugin-easing-quad@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-C2UJOca5MTDXKUTBXj30Kiqr5UyID+xrY/LxicVWWZPczQW2bBxbIbfq9ULvzGDwBTxE2rdvIB8YFKmDYO45qw=="], + + "@tsparticles/plugin-emitters": ["@tsparticles/plugin-emitters@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-h7opR8SoFWBmVHceDLJUerLENaPfkJSh2zQYvzmLj2L+V3VLS1QDgty+4QZVeZfqNROmgQw2eLFA5El1E0sqqw=="], + + "@tsparticles/plugin-emitters-shape-circle": ["@tsparticles/plugin-emitters-shape-circle@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1", "@tsparticles/plugin-emitters": "3.9.1" } }, "sha512-z+9MsAPWr++sNz6N6303rRDjusW0BIPhHY51E5eXGDcRdOqrESDs6y99AJ/6Kdb/PpibCIYjFY9jVi2JJADPRA=="], + + "@tsparticles/plugin-emitters-shape-square": ["@tsparticles/plugin-emitters-shape-square@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1", "@tsparticles/plugin-emitters": "3.9.1" } }, "sha512-dhA1c7FKs19B8lgTf25OTA3JoptNA+rjorsqCFuY1BZDI8g9E8DNqikUge14/W7nZN96+98hY+ghxSl4K2YsgA=="], + + "@tsparticles/plugin-hex-color": ["@tsparticles/plugin-hex-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-vZgZ12AjUicJvk7AX4K2eAmKEQX/D1VEjEPFhyjbgI7A65eX72M465vVKIgNA6QArLZ1DLs7Z787LOE6GOBWsg=="], + + "@tsparticles/plugin-hsl-color": ["@tsparticles/plugin-hsl-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-jJd1iGgRwX6eeNjc1zUXiJivaqC5UE+SC2A3/NtHwwoQrkfxGWmRHOsVyLnOBRcCPgBp/FpdDe6DIDjCMO715w=="], + + "@tsparticles/plugin-rgb-color": ["@tsparticles/plugin-rgb-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-SBxk7f1KBfXeTnnklbE2Hx4jBgh6I6HOtxb+Os1gTp0oaghZOkWcCD2dP4QbUu7fVNCMOcApPoMNC8RTFcy9wQ=="], + + "@tsparticles/plugin-sounds": ["@tsparticles/plugin-sounds@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-Hw2hmKmkkuFnNhOBwkvREXLULbAUwXXMeenyIy0jUGGuvjjI7rivJeFU6djCaaI0q4SbIlBFuu9mLZw2FEfwEA=="], + + "@tsparticles/shape-circle": ["@tsparticles/shape-circle@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-DqZFLjbuhVn99WJ+A9ajz9YON72RtCcvubzq6qfjFmtwAK7frvQeb6iDTp6Ze9FUipluxVZWVRG4vWTxi2B+/g=="], + + "@tsparticles/shape-emoji": ["@tsparticles/shape-emoji@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-ifvY63usuT+hipgVHb8gelBHSeF6ryPnMxAAEC1RGHhhXfpSRWMtE6ybr+pSsYU52M3G9+TF84v91pSwNrb9ZQ=="], + + "@tsparticles/shape-image": ["@tsparticles/shape-image@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-fCA5eme8VF3oX8yNVUA0l2SLDKuiZObkijb0z3Ky0qj1HUEVlAuEMhhNDNB9E2iELTrWEix9z7BFMePp2CC7AA=="], + + "@tsparticles/shape-line": ["@tsparticles/shape-line@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-wT8NSp0N9HURyV05f371cHKcNTNqr0/cwUu6WhBzbshkYGy1KZUP9CpRIh5FCrBpTev34mEQfOXDycgfG0KiLQ=="], + + "@tsparticles/shape-polygon": ["@tsparticles/shape-polygon@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-dA77PgZdoLwxnliH6XQM/zF0r4jhT01pw5y7XTeTqws++hg4rTLV9255k6R6eUqKq0FPSW1/WBsBIl7q/MmrqQ=="], + + "@tsparticles/shape-square": ["@tsparticles/shape-square@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-DKGkDnRyZrAm7T2ipqNezJahSWs6xd9O5LQLe5vjrYm1qGwrFxJiQaAdlb00UNrexz1/SA7bEoIg4XKaFa7qhQ=="], + + "@tsparticles/shape-star": ["@tsparticles/shape-star@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-kdMJpi8cdeb6vGrZVSxTG0JIjCwIenggqk0EYeKAwtOGZFBgL7eHhF2F6uu1oq8cJAbXPujEoabnLsz6mW8XaA=="], + + "@tsparticles/shape-text": ["@tsparticles/shape-text@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-oNsLHI0lGkIXoUw3W598iwd7dtoHCDrwpwJRGnQzgfk6T5a9dCpSD5vDeQN89lr3BUbVui4lhxq+/TyC64oAqA=="], + + "@tsparticles/slim": ["@tsparticles/slim@3.9.1", "", { "dependencies": { "@tsparticles/basic": "3.9.1", "@tsparticles/engine": "3.9.1", "@tsparticles/interaction-external-attract": "3.9.1", "@tsparticles/interaction-external-bounce": "3.9.1", "@tsparticles/interaction-external-bubble": "3.9.1", "@tsparticles/interaction-external-connect": "3.9.1", "@tsparticles/interaction-external-grab": "3.9.1", "@tsparticles/interaction-external-pause": "3.9.1", "@tsparticles/interaction-external-push": "3.9.1", "@tsparticles/interaction-external-remove": "3.9.1", "@tsparticles/interaction-external-repulse": "3.9.1", "@tsparticles/interaction-external-slow": "3.9.1", "@tsparticles/interaction-particles-attract": "3.9.1", "@tsparticles/interaction-particles-collisions": "3.9.1", "@tsparticles/interaction-particles-links": "3.9.1", "@tsparticles/move-parallax": "3.9.1", "@tsparticles/plugin-easing-quad": "3.9.1", "@tsparticles/shape-emoji": "3.9.1", "@tsparticles/shape-image": "3.9.1", "@tsparticles/shape-line": "3.9.1", "@tsparticles/shape-polygon": "3.9.1", "@tsparticles/shape-square": "3.9.1", "@tsparticles/shape-star": "3.9.1", "@tsparticles/updater-life": "3.9.1", "@tsparticles/updater-rotate": "3.9.1", "@tsparticles/updater-stroke-color": "3.9.1" } }, "sha512-CL5cDmADU7sDjRli0So+hY61VMbdroqbArmR9Av+c1Fisa5ytr6QD7Jv62iwU2S6rvgicEe9OyRmSy5GIefwZw=="], + + "@tsparticles/updater-color": ["@tsparticles/updater-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-XGWdscrgEMA8L5E7exsE0f8/2zHKIqnTrZymcyuFBw2DCB6BIV+5z6qaNStpxrhq3DbIxxhqqcybqeOo7+Alpg=="], + + "@tsparticles/updater-destroy": ["@tsparticles/updater-destroy@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-MjMzEhZwCQIbxO6ZRM0eXsHVwmlXuUqwC43WCPZCpjhK3AJrMu3KR4xsJieFTWIbVNguAvbgoTB10FfJOUU5VA=="], + + "@tsparticles/updater-life": ["@tsparticles/updater-life@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-Oi8aF2RIwMMsjssUkCB6t3PRpENHjdZf6cX92WNfAuqXtQphr3OMAkYFJFWkvyPFK22AVy3p/cFt6KE5zXxwAA=="], + + "@tsparticles/updater-opacity": ["@tsparticles/updater-opacity@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-w778LQuRZJ+IoWzeRdrGykPYSSaTeWfBvLZ2XwYEkh/Ss961InOxZKIpcS6i5Kp/Zfw0fS1ZAuqeHwuj///Osw=="], + + "@tsparticles/updater-out-modes": ["@tsparticles/updater-out-modes@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-cKQEkAwbru+hhKF+GTsfbOvuBbx2DSB25CxOdhtW2wRvDBoCnngNdLw91rs+0Cex4tgEeibkebrIKFDDE6kELg=="], + + "@tsparticles/updater-roll": ["@tsparticles/updater-roll@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-zl4JeM3gUBJ0uttmIsond3lrZ3f3AkItFeS0Lhj/7jiCKfUoRyyOMrcBk8R1AhW7lI+7ko1iBs3jhO0jnxz9vg=="], + + "@tsparticles/updater-rotate": ["@tsparticles/updater-rotate@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-9BfKaGfp28JN82MF2qs6Ae/lJr9EColMfMTHqSKljblwbpVDHte4umuwKl3VjbRt87WD9MGtla66NTUYl+WxuQ=="], + + "@tsparticles/updater-size": ["@tsparticles/updater-size@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-3NSVs0O2ApNKZXfd+y/zNhTXSFeG1Pw4peI8e6z/q5+XLbmue9oiEwoPy/tQLaark3oNj3JU7Q903ZijPyXSzw=="], + + "@tsparticles/updater-stroke-color": ["@tsparticles/updater-stroke-color@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-3x14+C2is9pZYTg9T2TiA/aM1YMq4wLdYaZDcHm3qO30DZu5oeQq0rm/6w+QOGKYY1Z3Htg9rlSUZkhTHn7eDA=="], + + "@tsparticles/updater-tilt": ["@tsparticles/updater-tilt@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-PB2yaoyXRmSk4iIVgjtRrzOxXMK9mjeAQHIJGtT4faq46Z8cbIIEFgjTwqrUV8qOrNg/h4sm5NE/s0qsTYjp1Q=="], + + "@tsparticles/updater-twinkle": ["@tsparticles/updater-twinkle@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-xgTcYr6LmP44IPIBeQmEExN2Y5Nfl3ikmC08eOh5nZy/ta6ORP+JTsprrnfuv/O2DwTyoqFLkZ16hZfkdc1yOQ=="], + + "@tsparticles/updater-wobble": ["@tsparticles/updater-wobble@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1" } }, "sha512-c99Ogy9q4QWO+zsDXol0UnpUwZiY2UucFb8ltuDv9AlbGUeprygoub8jhgT5pEDv+GdzWOJGSgq7rfgv9cHBrg=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.8.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + + "@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="], + + "@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/dompurify": ["@types/dompurify@3.2.0", "", { "dependencies": { "dompurify": "*" } }, "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], + + "@types/marked": ["@types/marked@6.0.0", "", { "dependencies": { "marked": "*" } }, "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="], + + "@types/papaparse": ["@types/papaparse@5.5.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA=="], + + "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], + + "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + + "@types/simple-oauth2": ["@types/simple-oauth2@5.0.8", "", {}, "sha512-TehQqoOGdy3/rmFsCEGgnt1f4JhUCA0joWemGGCTbVYvoZvfBjkRsBFYmz8k0V/sn2XQZHe33L4lWxqMhIO3tQ=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], + + "@types/xml2js": ["@types/xml2js@0.4.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.58.1", "", {}, "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw=="], + + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.4", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.4", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.4", "vitest": "4.1.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w=="], + + "@vitest/expect": ["@vitest/expect@4.1.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.4", "", { "dependencies": { "@vitest/spy": "4.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.4", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A=="], + + "@vitest/runner": ["@vitest/runner@4.1.4", "", { "dependencies": { "@vitest/utils": "4.1.4", "pathe": "^2.0.3" } }, "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.4", "", { "dependencies": { "@vitest/pretty-format": "4.1.4", "@vitest/utils": "4.1.4", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw=="], + + "@vitest/spy": ["@vitest/spy@4.1.4", "", {}, "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ=="], + + "@vitest/utils": ["@vitest/utils@4.1.4", "", { "dependencies": { "@vitest/pretty-format": "4.1.4", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "abortcontroller-polyfill": ["abortcontroller-polyfill@1.7.8", "", {}, "sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "airtable": ["airtable@0.12.2", "", { "dependencies": { "@types/node": ">=8.0.0 <15", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.4.0", "lodash": "^4.17.21", "node-fetch": "^2.6.7" } }, "sha512-HS3VytUBTKj8A0vPl7DDr5p/w3IOGv6RXL0fv7eczOWAtj9Xe8ri4TAiZRXoOyo+Z/COADCj+oARFenbxhmkIg=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg=="], + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + "bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="], + + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "canvas-confetti": ["canvas-confetti@1.9.4", "", {}, "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "cssstyle": ["cssstyle@6.2.0", "", { "dependencies": { "@asamuzakjp/css-color": "^5.0.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.28", "css-tree": "^3.1.0", "lru-cache": "^11.2.6" } }, "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig=="], + + "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - "dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="], + "devalue": ["devalue@5.7.1", "", {}, "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA=="], + + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + + "dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], + + "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "error-causes": ["error-causes@3.0.2", "", {}, "sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw=="], + + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], - "esrap": ["esrap@2.2.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg=="], + "esrap": ["esrap@2.2.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@typescript-eslint/types": "^8.2.0" } }, "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fs-monkey": ["fs-monkey@1.1.0", "", {}, "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], + + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="], + + "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + + "jsdom": ["jsdom@28.1.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", "@bramus/specificity": "^2.4.2", "@exodus/bytes": "^1.11.0", "cssstyle": "^6.0.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.21.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug=="], + + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], + + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + + "lru-cache": ["lru-cache@11.3.3", "", {}, "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ=="], + + "lucia": ["lucia@3.2.2", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0" } }, "sha512-P1FlFBGCMPMXu+EGdVD9W4Mjm0DqsusmKgO7Xc33mI5X1bklmsQb0hfzPhXomQr9waWIBDsiOjvr1e6BTaUqpA=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + + "memfs": ["memfs@3.5.3", "", { "dependencies": { "fs-monkey": "^1.0.4" } }, "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw=="], + + "memfs-browser": ["memfs-browser@3.5.10302", "", { "dependencies": { "memfs": "3.5.3" } }, "sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], @@ -194,38 +709,294 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "oslo": ["oslo@1.2.1", "", { "dependencies": { "@node-rs/argon2": "1.7.0", "@node-rs/bcrypt": "1.9.0" } }, "sha512-HfIhB5ruTdQv0XX2XlncWQiJ5SIHZ7NHZhVyHth0CSZ/xzge00etRyYy/3wp/Dsu+PkxMC+6+B2lS/GcKoewkA=="], + + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "papaparse": ["papaparse@5.5.3", "", {}, "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="], + + "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pdf-lib": ["pdf-lib@1.17.1", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "pako": "^1.0.11", "tslib": "^1.11.1" } }, "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw=="], + + "pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], + + "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], + + "pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], + + "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "simple-oauth2": ["simple-oauth2@5.1.0", "", { "dependencies": { "@hapi/hoek": "^11.0.4", "@hapi/wreck": "^18.0.0", "debug": "^4.3.4", "joi": "^17.6.4" } }, "sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw=="], "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "svelte": ["svelte@5.46.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], + + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "svelte": ["svelte@5.55.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-z41M/hi0ZPTzrwVKLvB/R1/Oo08gL1uIib8HZ+FncqxxtY9MLb01emg2fqk+WLZ/lNrrtNDFh7BZLDxAHvMgLw=="], "svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="], + "svelte-marked": ["svelte-marked@0.8.0", "", { "dependencies": { "@types/marked": "6.0.0", "github-slugger": "2.0.0", "marked": "15.0.7" }, "peerDependencies": { "svelte": "^5" } }, "sha512-qOtPoMTZS4Syq+Tfx3Eux/M73lHETSQHrkgLPY6/LeTNQL3bQ+ckS/+DyV1B9HAy5KVUH/qxGypWiO7nsFMa1Q=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "tldts": ["tldts@7.0.28", "", { "dependencies": { "tldts-core": "^7.0.28" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw=="], + + "tldts-core": ["tldts-core@7.0.28", "", {}, "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ=="], + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + + "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "tsparticles": ["tsparticles@3.9.1", "", { "dependencies": { "@tsparticles/engine": "3.9.1", "@tsparticles/interaction-external-trail": "3.9.1", "@tsparticles/plugin-absorbers": "3.9.1", "@tsparticles/plugin-emitters": "3.9.1", "@tsparticles/plugin-emitters-shape-circle": "3.9.1", "@tsparticles/plugin-emitters-shape-square": "3.9.1", "@tsparticles/shape-text": "3.9.1", "@tsparticles/slim": "3.9.1", "@tsparticles/updater-destroy": "3.9.1", "@tsparticles/updater-roll": "3.9.1", "@tsparticles/updater-tilt": "3.9.1", "@tsparticles/updater-twinkle": "3.9.1", "@tsparticles/updater-wobble": "3.9.1" } }, "sha512-Y780IGSL4qjkZj7+fI92PV/cziHqLR/s6nnYri4K6vH3NQRmDK5D6pfskDO8T4Y96ChCWHY3uxPtOb/hKQ83Qg=="], + + "tsparticles-basic": ["tsparticles-basic@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0", "tsparticles-move-base": "^2.12.0", "tsparticles-shape-circle": "^2.12.0", "tsparticles-updater-color": "^2.12.0", "tsparticles-updater-opacity": "^2.12.0", "tsparticles-updater-out-modes": "^2.12.0", "tsparticles-updater-size": "^2.12.0" } }, "sha512-pN6FBpL0UsIUXjYbiui5+IVsbIItbQGOlwyGV55g6IYJBgdTNXgFX0HRYZGE9ZZ9psEXqzqwLM37zvWnb5AG9g=="], + + "tsparticles-engine": ["tsparticles-engine@2.12.0", "", {}, "sha512-ZjDIYex6jBJ4iMc9+z0uPe7SgBnmb6l+EJm83MPIsOny9lPpetMsnw/8YJ3xdxn8hV+S3myTpTN1CkOVmFv0QQ=="], + + "tsparticles-move-base": ["tsparticles-move-base@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-oSogCDougIImq+iRtIFJD0YFArlorSi8IW3HD2gO3USkH+aNn3ZqZNTqp321uB08K34HpS263DTbhLHa/D6BWw=="], + + "tsparticles-plugin-emitters": ["tsparticles-plugin-emitters@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-fbskYnaXWXivBh9KFReVCfqHdhbNQSK2T+fq2qcGEWpwtDdgujcaS1k2Q/xjZnWNMfVesik4IrqspcL51gNdSA=="], + + "tsparticles-plugin-sounds": ["tsparticles-plugin-sounds@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-Jm/Mdddq9E9VdHtAbYmv3pgtHQ3nBSbXN+n2axqm6vACgrE/JA4tgLZyuknns0scxibvqh/7zFxkOSa+4ucYuQ=="], + + "tsparticles-preset-fireworks": ["tsparticles-preset-fireworks@2.12.0", "", { "dependencies": { "tsparticles-basic": "^2.12.0", "tsparticles-engine": "^2.12.0", "tsparticles-plugin-emitters": "^2.12.0", "tsparticles-plugin-sounds": "^2.12.0", "tsparticles-shape-line": "^2.12.0", "tsparticles-updater-destroy": "^2.12.0", "tsparticles-updater-life": "^2.12.0", "tsparticles-updater-rotate": "^2.12.0", "tsparticles-updater-stroke-color": "^2.12.0" } }, "sha512-8Y1F2YZH/LkqPVniEmfArHe2qyuJwzTiWmX1of9bClyA+Xz7p219JRednNp0QaAiZeJ+GS9X7hlBa4r3m1kPiw=="], + + "tsparticles-shape-circle": ["tsparticles-shape-circle@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-L6OngbAlbadG7b783x16ns3+SZ7i0SSB66M8xGa5/k+YcY7zm8zG0uPt1Hd+xQDR2aNA3RngVM10O23/Lwk65Q=="], + + "tsparticles-shape-line": ["tsparticles-shape-line@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-RcpKmmpKlk+R8mM5wA2v64Lv1jvXtU4SrBDv3vbdRodKbKaWGGzymzav1Q0hYyDyUZgplEK/a5ZwrfrOwmgYGA=="], + + "tsparticles-updater-color": ["tsparticles-updater-color@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-KcG3a8zd0f8CTiOrylXGChBrjhKcchvDJjx9sp5qpwQK61JlNojNCU35xoaSk2eEHeOvFjh0o3CXWUmYPUcBTQ=="], + + "tsparticles-updater-destroy": ["tsparticles-updater-destroy@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-6NN3dJhxACvzbIGL4dADbYQSZJmdHfwjujj1uvnxdMbb2x8C/AZzGxiN33smo4jkrZ5VLEWZWCJPJ8aOKjQ2Sg=="], + + "tsparticles-updater-life": ["tsparticles-updater-life@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-J7RWGHAZkowBHpcLpmjKsxwnZZJ94oGEL2w+wvW1/+ZLmAiFFF6UgU0rHMC5CbHJT4IPx9cbkYMEHsBkcRJ0Bw=="], + + "tsparticles-updater-opacity": ["tsparticles-updater-opacity@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-YUjMsgHdaYi4HN89LLogboYcCi1o9VGo21upoqxq19yRy0hRCtx2NhH22iHF/i5WrX6jqshN0iuiiNefC53CsA=="], + + "tsparticles-updater-out-modes": ["tsparticles-updater-out-modes@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-owBp4Gk0JNlSrmp12XVEeBroDhLZU+Uq3szbWlHGSfcR88W4c/0bt0FiH5bHUqORIkw+m8O56hCjbqwj69kpOQ=="], + + "tsparticles-updater-rotate": ["tsparticles-updater-rotate@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-waOFlGFmEZOzsQg4C4VSejNVXGf4dMf3fsnQrEROASGf1FCd8B6WcZau7JtXSTFw0OUGuk8UGz36ETWN72DkCw=="], + + "tsparticles-updater-size": ["tsparticles-updater-size@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-B0yRdEDd/qZXCGDL/ussHfx5YJ9UhTqNvmS5X2rR2hiZhBAE2fmsXLeWkdtF2QusjPeEqFDxrkGiLOsh6poqRA=="], + + "tsparticles-updater-stroke-color": ["tsparticles-updater-stroke-color@2.12.0", "", { "dependencies": { "tsparticles-engine": "^2.12.0" } }, "sha512-MPou1ZDxsuVq6SN1fbX+aI5yrs6FyP2iPCqqttpNbWyL+R6fik1rL0ab/x02B57liDXqGKYomIbBQVP3zUTW1A=="], + + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "undici": ["undici@7.24.7", "", {}, "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ=="], + + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + "vitest": ["vitest@4.1.4", "", { "dependencies": { "@vitest/expect": "4.1.4", "@vitest/mocker": "4.1.4", "@vitest/pretty-format": "4.1.4", "@vitest/runner": "4.1.4", "@vitest/snapshot": "4.1.4", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.4", "@vitest/browser-preview": "4.1.4", "@vitest/browser-webdriverio": "4.1.4", "@vitest/coverage-istanbul": "4.1.4", "@vitest/coverage-v8": "4.1.4", "@vitest/ui": "4.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], + + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@hapi/topo/@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], + + "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], + + "@sideway/address/@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], + + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@types/marked/marked": ["marked@15.0.7", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg=="], + + "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "ast-v8-to-istanbul/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "joi/@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], + + "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "svelte/aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], + + "svelte-marked/marked": ["marked@15.0.7", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg=="], + + "vite/rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], + + "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "vite/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], + + "vite/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="], + + "vite/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw=="], + + "vite/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A=="], + + "vite/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.54.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA=="], + + "vite/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ=="], + + "vite/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ=="], + + "vite/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA=="], + + "vite/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng=="], + + "vite/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg=="], + + "vite/rollup/@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw=="], + + "vite/rollup/@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA=="], + + "vite/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ=="], + + "vite/rollup/@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A=="], + + "vite/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ=="], + + "vite/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ=="], + + "vite/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw=="], + + "vite/rollup/@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg=="], + + "vite/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw=="], + + "vite/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ=="], + + "vite/rollup/@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ=="], + + "vite/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], } } diff --git a/resolution-frontend/drizzle/0002_add_package_type_and_orders.sql b/resolution-frontend/drizzle/0002_add_package_type_and_orders.sql new file mode 100644 index 0000000..42e13d9 --- /dev/null +++ b/resolution-frontend/drizzle/0002_add_package_type_and_orders.sql @@ -0,0 +1,43 @@ +ALTER TABLE "warehouse_item" ADD COLUMN "package_type" text DEFAULT 'box' NOT NULL; +--> statement-breakpoint +CREATE TYPE "warehouse_order_status" AS ENUM ('DRAFT', 'ESTIMATED', 'APPROVED', 'SHIPPED', 'CANCELLED'); +--> statement-breakpoint +CREATE TABLE "warehouse_order" ( + "id" text PRIMARY KEY NOT NULL, + "created_by_id" text NOT NULL, + "status" "warehouse_order_status" DEFAULT 'DRAFT' NOT NULL, + "first_name" text NOT NULL, + "last_name" text NOT NULL, + "email" text NOT NULL, + "phone" text, + "address_line_1" text NOT NULL, + "address_line_2" text, + "city" text NOT NULL, + "state_province" text NOT NULL, + "postal_code" text, + "country" text NOT NULL, + "estimated_shipping_cents" integer, + "estimated_service_name" text, + "estimated_package_type" text, + "estimated_total_length_in" real, + "estimated_total_width_in" real, + "estimated_total_height_in" real, + "estimated_total_weight_grams" real, + "notes" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "warehouse_order_item" ( + "id" text PRIMARY KEY NOT NULL, + "order_id" text NOT NULL, + "warehouse_item_id" text NOT NULL, + "quantity" integer DEFAULT 1 NOT NULL, + "sizing_choice" text +); +--> statement-breakpoint +ALTER TABLE "warehouse_order" ADD CONSTRAINT "warehouse_order_created_by_id_user_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +ALTER TABLE "warehouse_order_item" ADD CONSTRAINT "warehouse_order_item_order_id_warehouse_order_id_fk" FOREIGN KEY ("order_id") REFERENCES "public"."warehouse_order"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +ALTER TABLE "warehouse_order_item" ADD CONSTRAINT "warehouse_order_item_warehouse_item_id_warehouse_item_id_fk" FOREIGN KEY ("warehouse_item_id") REFERENCES "public"."warehouse_item"("id") ON DELETE restrict ON UPDATE no action; diff --git a/resolution-frontend/drizzle/0003_add_templates_and_batches.sql b/resolution-frontend/drizzle/0003_add_templates_and_batches.sql new file mode 100644 index 0000000..7044b7e --- /dev/null +++ b/resolution-frontend/drizzle/0003_add_templates_and_batches.sql @@ -0,0 +1,55 @@ +-- Order Templates +CREATE TABLE "warehouse_order_template" ( + "id" text PRIMARY KEY NOT NULL, + "created_by_id" text NOT NULL, + "name" text NOT NULL, + "is_public" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "warehouse_order_template_item" ( + "id" text PRIMARY KEY NOT NULL, + "template_id" text NOT NULL, + "warehouse_item_id" text NOT NULL, + "quantity" integer DEFAULT 1 NOT NULL +); +--> statement-breakpoint +ALTER TABLE "warehouse_order_template" ADD CONSTRAINT "warehouse_order_template_created_by_id_user_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +ALTER TABLE "warehouse_order_template_item" ADD CONSTRAINT "warehouse_order_template_item_template_id_warehouse_order_template_id_fk" FOREIGN KEY ("template_id") REFERENCES "public"."warehouse_order_template"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +ALTER TABLE "warehouse_order_template_item" ADD CONSTRAINT "warehouse_order_template_item_warehouse_item_id_warehouse_item_id_fk" FOREIGN KEY ("warehouse_item_id") REFERENCES "public"."warehouse_item"("id") ON DELETE restrict ON UPDATE no action; +--> statement-breakpoint +-- Batches +CREATE TYPE "warehouse_batch_status" AS ENUM ('AWAITING_MAPPING', 'MAPPED', 'PROCESSED'); +--> statement-breakpoint +CREATE TABLE "warehouse_batch" ( + "id" text PRIMARY KEY NOT NULL, + "created_by_id" text NOT NULL, + "template_id" text NOT NULL, + "title" text, + "status" "warehouse_batch_status" DEFAULT 'AWAITING_MAPPING' NOT NULL, + "csv_data" text NOT NULL, + "field_mapping" text, + "address_count" integer DEFAULT 0 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "warehouse_batch_tag" ( + "id" text PRIMARY KEY NOT NULL, + "batch_id" text NOT NULL, + "tag" text NOT NULL +); +--> statement-breakpoint +ALTER TABLE "warehouse_batch" ADD CONSTRAINT "warehouse_batch_created_by_id_user_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +ALTER TABLE "warehouse_batch" ADD CONSTRAINT "warehouse_batch_template_id_warehouse_order_template_id_fk" FOREIGN KEY ("template_id") REFERENCES "public"."warehouse_order_template"("id") ON DELETE restrict ON UPDATE no action; +--> statement-breakpoint +ALTER TABLE "warehouse_batch_tag" ADD CONSTRAINT "warehouse_batch_tag_batch_id_warehouse_batch_id_fk" FOREIGN KEY ("batch_id") REFERENCES "public"."warehouse_batch"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +CREATE UNIQUE INDEX "warehouse_batch_tag_unique_idx" ON "warehouse_batch_tag" USING btree ("batch_id","tag"); +--> statement-breakpoint +-- Add batch_id to warehouse_order +ALTER TABLE "warehouse_order" ADD COLUMN "batch_id" text; diff --git a/resolution-frontend/drizzle/0004_add_label_tracking_fields.sql b/resolution-frontend/drizzle/0004_add_label_tracking_fields.sql new file mode 100644 index 0000000..7b1af83 --- /dev/null +++ b/resolution-frontend/drizzle/0004_add_label_tracking_fields.sql @@ -0,0 +1,3 @@ +ALTER TABLE "warehouse_order" ADD COLUMN "tracking_number" text; +ALTER TABLE "warehouse_order" ADD COLUMN "label_url" text; +ALTER TABLE "warehouse_order" ADD COLUMN "shipping_method" text; diff --git a/resolution-frontend/drizzle/0005_add_hs_code.sql b/resolution-frontend/drizzle/0005_add_hs_code.sql new file mode 100644 index 0000000..0260b5a --- /dev/null +++ b/resolution-frontend/drizzle/0005_add_hs_code.sql @@ -0,0 +1 @@ +ALTER TABLE "warehouse_item" ADD COLUMN "hs_code" text NOT NULL DEFAULT ''; diff --git a/resolution-frontend/drizzle/meta/0002_snapshot.json b/resolution-frontend/drizzle/meta/0002_snapshot.json index fb049ac..1cebc5d 100644 --- a/resolution-frontend/drizzle/meta/0002_snapshot.json +++ b/resolution-frontend/drizzle/meta/0002_snapshot.json @@ -1,5 +1,5 @@ { - "id": "35dc54d2-783c-4816-8106-c7e56ffd8939", + "id": "0fa6b235-2de1-4477-8997-b6da9895316e", "prevId": "becd34cd-7692-4ef3-a912-458e58e190ad", "version": "7", "dialect": "postgresql", @@ -1058,6 +1058,90 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.warehouse_item": { + "name": "warehouse_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sizing": { + "name": "sizing", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight_grams": { + "name": "weight_grams", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "warehouse_item_sku_unique": { + "name": "warehouse_item_sku_unique", + "nullsNotDistinct": false, + "columns": [ + "sku" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.weekly_ship": { "name": "weekly_ship", "schema": "", @@ -1564,6 +1648,7 @@ "schema": "public", "values": [ "PYTHON", + "WEB_DEV", "RUST", "GAME_DEV", "HARDWARE", diff --git a/resolution-frontend/drizzle/meta/_journal.json b/resolution-frontend/drizzle/meta/_journal.json index 22a6c0c..75c06c7 100644 --- a/resolution-frontend/drizzle/meta/_journal.json +++ b/resolution-frontend/drizzle/meta/_journal.json @@ -19,8 +19,15 @@ { "idx": 2, "version": "7", - "when": 1772636037511, - "tag": "0002_luxuriant_maria_hill", + "when": 1772206185519, + "tag": "0002_add_package_type_and_orders", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1772790000000, + "tag": "0003_add_templates_and_batches", "breakpoints": true } ] diff --git a/resolution-frontend/entrypoint.sh b/resolution-frontend/entrypoint.sh new file mode 100644 index 0000000..ef44e53 --- /dev/null +++ b/resolution-frontend/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +echo "Running database schema push..." +echo "" | npx drizzle-kit push --verbose 2>&1 || echo "WARNING: drizzle-kit push failed with exit code $?" +echo "Schema push complete." + +exec node build diff --git a/resolution-frontend/package-lock.json b/resolution-frontend/package-lock.json index 6ecccad..e8bdf3d 100644 --- a/resolution-frontend/package-lock.json +++ b/resolution-frontend/package-lock.json @@ -17,19 +17,21 @@ "airtable": "^0.12.2", "bcrypt": "^6.0.0", "canvas-confetti": "^1.9.4", - "dompurify": "^3.3.2", + "dompurify": "^3.3.3", "dotenv": "^16.4.0", "drizzle-orm": "^0.45.1", "jsonwebtoken": "^9.0.3", "lucia": "^3.2.2", "monaco-editor": "^0.55.1", "oslo": "^1.2.1", + "pdf-lib": "^1.17.1", "pg": "^8.13.0", "simple-oauth2": "^5.1.0", "svelte-marked": "^0.8.0", "tsparticles": "^3.9.1", "tsparticles-preset-fireworks": "^2.12.0", "uuid": "^13.0.0", + "xml2js": "^0.6.2", "zod": "^4.3.5" }, "devDependencies": { @@ -44,6 +46,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/pg": "^8.16.0", "@types/simple-oauth2": "^5.0.8", + "@types/xml2js": "^0.4.14", "@vitest/coverage-v8": "^4.0.18", "drizzle-kit": "^0.31.8", "jsdom": "^28.1.0", @@ -292,7 +295,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -333,7 +335,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -1516,6 +1517,24 @@ "cuid2": "bin/cuid2.js" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "dev": true, @@ -2046,7 +2065,6 @@ "integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -2087,7 +2105,6 @@ "version": "6.2.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -2975,7 +2992,6 @@ "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -3006,6 +3022,16 @@ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "license": "MIT" }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitest/coverage-v8": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", @@ -3179,7 +3205,6 @@ "node_modules/acorn": { "version": "8.15.0", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3588,7 +3613,6 @@ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz", "integrity": "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", @@ -3751,7 +3775,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4284,7 +4307,6 @@ "integrity": "sha512-P1FlFBGCMPMXu+EGdVD9W4Mjm0DqsusmKgO7Xc33mI5X1bklmsQb0hfzPhXomQr9waWIBDsiOjvr1e6BTaUqpA==", "deprecated": "This package has been deprecated. Please see https://lucia-auth.com/lucia-v3/migrate.", "license": "MIT", - "peer": true, "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0" @@ -4508,6 +4530,12 @@ "@node-rs/bcrypt": "1.9.0" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -4535,12 +4563,29 @@ "dev": true, "license": "MIT" }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/pg": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", @@ -4812,7 +4857,6 @@ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -4883,6 +4927,15 @@ ], "license": "MIT" }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -5049,7 +5102,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.6.tgz", "integrity": "sha512-lP5DGF3oDDI9fhHcSpaBiJEkFLuS16h92DhM1L5K1lFm0WjOmUh1i2sNkBBk8rkxJRpob0dBE75jRfUzGZUOGA==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -5485,7 +5537,6 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5528,7 +5579,6 @@ "version": "7.3.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -5622,7 +5672,6 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -5761,6 +5810,28 @@ "node": ">=18" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/resolution-frontend/package.json b/resolution-frontend/package.json index c36fcea..bd7f3b0 100644 --- a/resolution-frontend/package.json +++ b/resolution-frontend/package.json @@ -23,8 +23,10 @@ "@types/bcrypt": "^6.0.0", "@types/dompurify": "^3.2.0", "@types/jsonwebtoken": "^9.0.10", + "@types/papaparse": "^5.5.2", "@types/pg": "^8.16.0", "@types/simple-oauth2": "^5.0.8", + "@types/xml2js": "^0.4.14", "@vitest/coverage-v8": "^4.0.18", "drizzle-kit": "^0.31.8", "jsdom": "^28.1.0", @@ -44,19 +46,22 @@ "airtable": "^0.12.2", "bcrypt": "^6.0.0", "canvas-confetti": "^1.9.4", - "dompurify": "^3.3.2", + "dompurify": "^3.3.3", "dotenv": "^16.4.0", "drizzle-orm": "^0.45.1", "jsonwebtoken": "^9.0.3", "lucia": "^3.2.2", "monaco-editor": "^0.55.1", "oslo": "^1.2.1", + "papaparse": "^5.5.3", + "pdf-lib": "^1.17.1", "pg": "^8.13.0", "simple-oauth2": "^5.1.0", "svelte-marked": "^0.8.0", "tsparticles": "^3.9.1", "tsparticles-preset-fireworks": "^2.12.0", "uuid": "^13.0.0", + "xml2js": "^0.6.2", "zod": "^4.3.5" }, "overrides": { diff --git a/resolution-frontend/src/app.html b/resolution-frontend/src/app.html index 52d36b8..bf39263 100644 --- a/resolution-frontend/src/app.html +++ b/resolution-frontend/src/app.html @@ -7,6 +7,7 @@ + %sveltekit.head% diff --git a/resolution-frontend/src/lib/server/canada-post.ts b/resolution-frontend/src/lib/server/canada-post.ts new file mode 100644 index 0000000..7374cf2 --- /dev/null +++ b/resolution-frontend/src/lib/server/canada-post.ts @@ -0,0 +1,736 @@ +import { env } from '$env/dynamic/private'; +import xml2js from 'xml2js'; +import { PDFDocument } from 'pdf-lib'; +import { fetchChitChatsRates } from './chit-chats'; +import { resolveStateCode } from './countries'; +import { getCadToUsdRate } from './exchange-rate'; +import { arrayBufferToBase64 } from './utils'; + +export const INCHES_TO_CM = 2.54; +export const GRAMS_TO_KG = 0.001; + +export function escapeXml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** Format HS code to Canada Post format ####.##.##.## (6, 8 or 10 digits with dots) */ +function formatHsTariffCode(code: string | null | undefined): string { + if (!code) return ''; + const digits = code.replace(/[^0-9]/g, ''); + if (digits.length < 6) return ''; + // ####.## (6 digits) + let formatted = digits.substring(0, 4) + '.' + digits.substring(4, 6); + // ####.##.## (8 digits) + if (digits.length >= 8) formatted += '.' + digits.substring(6, 8); + // ####.##.##.## (10 digits) + if (digits.length >= 10) formatted += '.' + digits.substring(8, 10); + return formatted; +} + +async function cropLabelTo4x6(pdfBuffer: ArrayBuffer): Promise { + const srcDoc = await PDFDocument.load(pdfBuffer); + const page = srcDoc.getPage(0); + + // Canada Post non-contract labels are landscape 792x612 (11x8.5in) + // Label content is in the right portion of the page + // Crop tightly to the label borders for thermal printing + const cropX = 448; + const cropY = 100; + const cropWidth = 330; + const cropHeight = 445; + + page.setMediaBox(cropX, cropY, cropWidth, cropHeight); + page.setCropBox(cropX, cropY, cropWidth, cropHeight); + // Do NOT call page.setSize() - it overrides MediaBox and breaks the crop + + return await srcDoc.save(); +} + +export function inchesToCm(inches: number): number { + return Math.round(inches * INCHES_TO_CM * 10) / 10; +} + +export function isLettermail(serviceName: string | null): boolean { + if (!serviceName) return false; + const lower = serviceName.toLowerCase(); + return lower.includes('lettermail') || lower.includes('bubble packet'); +} + +export function buildDestinationXml(country: string, postalCode?: string, stateCode?: string): string { + if (country === 'CA') { + return ` + ${(postalCode ?? '').replace(/\s/g, '').toUpperCase()} + `; + } else if (country === 'US') { + return ` + ${(postalCode ?? '').replace(/\s/g, '')} + ${stateCode ? `${escapeXml(stateCode)}` : ''} + `; + } else { + return ` + ${escapeXml(country)} + ${postalCode ? `${escapeXml(postalCode)}` : ''} + `; + } +} + +export function buildRateRequestXml(params: { + originPostal: string; + country: string; + postalCode?: string; + weightKg: number; + lengthCm: number; + widthCm: number; + heightCm: number; +}): string { + const { originPostal, country, postalCode, weightKg, lengthCm, widthCm, heightCm } = params; + return ` + + ${env.CP_CUSTOMER_NUMBER} + ${env.CP_CONTRACT_ID ? `${env.CP_CONTRACT_ID}` : ''} + + ${Math.round(weightKg * 100) / 100} + + ${lengthCm} + ${widthCm} + ${heightCm} + + + ${originPostal.replace(/\s/g, '').toUpperCase()} + + ${buildDestinationXml(country, postalCode)} + +`; +} + +export function buildCreateShipmentXml(params: { + order: any; + weightKg: number; + lengthCm: number; + widthCm: number; + heightCm: number; + serviceCode: string; +}): string { + const { order, weightKg, lengthCm, widthCm, heightCm, serviceCode } = params; + const originPostal = (env.CP_ORIGIN_POSTAL_CODE || '').replace(/\s/g, '').toUpperCase(); + const customerNumber = env.CP_CUSTOMER_NUMBER; + const contractId = env.CP_CONTRACT_ID; + + const destinationXml = buildDestinationXml(order.country, order.postalCode, order.stateProvince); + + let customsXml = ''; + if (order.country !== 'CA') { + const items = (order.items || []).filter((oi: any) => oi.warehouseItem); + let skuLines: string; + if (items.length > 0) { + skuLines = items.map((oi: any) => { + const item = oi.warehouseItem; + const unitWeightKg = Math.round(item.weightGrams * GRAMS_TO_KG * 1000) / 1000; + const valuePerUnit = Math.round(item.costCents) / 100; + return ` + ${oi.quantity} + ${escapeXml(item.name.substring(0, 45))} + ${escapeXml((item.sku || '').substring(0, 15))} + ${escapeXml(formatHsTariffCode(item.hsCode))} + ${unitWeightKg} + ${valuePerUnit.toFixed(2)} + CA + `; + }).join('\n'); + } else { + skuLines = ` + 1 + Merchandise + ${Math.max(0.01, Math.round(weightKg * 1000) / 1000)} + 1.00 + CA + `; + } + + customsXml = ` + CAD + SOG + Merchandise + ${skuLines} + `; + } + + const deliverySpecXml = ` + ${escapeXml(serviceCode)} + + ${escapeXml(env.CP_SENDER_NAME || 'Hack Club')} + ${escapeXml(env.CP_SENDER_NAME || 'Hack Club')} + ${escapeXml(env.CP_SENDER_PHONE || '000-000-0000')} + + ${escapeXml(env.CP_SENDER_ADDRESS || '')} + ${env.CP_SENDER_ADDRESS_2 ? `${escapeXml(env.CP_SENDER_ADDRESS_2)}` : ''} + ${escapeXml(env.CP_SENDER_CITY || '')} + ${escapeXml(env.CP_SENDER_PROVINCE || '')} + ${contractId ? 'CA' : ''} + ${originPostal} + + + + ${escapeXml(`${order.firstName} ${order.lastName}`.substring(0, 44))} + ${escapeXml((order.phone || env.CP_SENDER_PHONE || '000-000-0000').substring(0, 25))} + + ${escapeXml((order.addressLine1 || '').substring(0, 44))} + ${(order.addressLine2 || order.addressLine1?.length > 44) ? `${escapeXml((order.addressLine2 || order.addressLine1?.substring(44) || '').substring(0, 44))}` : ''} + ${escapeXml((order.city || '').substring(0, 40))} + ${escapeXml(resolveStateCode(order.stateProvince || '').substring(0, 20))} + ${escapeXml(order.country)} + ${(order.postalCode ?? '').replace(/\s/g, '').toUpperCase()} + + + ${order.country !== 'CA' ? ` + + ` : ''} + + ${Math.max(0.01, Math.round(weightKg * 1000) / 1000)} + + ${Math.max(1, lengthCm)} + ${Math.max(1, widthCm)} + ${Math.max(1, heightCm)} + + + ${contractId ? ` + 4x6 + PDF + ` : ''} + + false + + ${customsXml} + ${contractId ? ` + ${customerNumber} + ${contractId} + Account + ` : ''} + `; + + if (contractId) { + return ` + + + ${originPostal} + true + ${deliverySpecXml} +`; + } + + return ` + + ${originPostal} + ${deliverySpecXml} +`; +} + +export function getServiceCode(serviceName: string): string { + const lower = serviceName.toLowerCase(); + if (lower.includes('priority')) return 'DOM.PC'; + if (lower.includes('xpresspost') && lower.includes('international')) return 'INT.XP'; + if (lower.includes('xpresspost')) return 'DOM.XP'; + if (lower.includes('expedited') && lower.includes('usa')) return 'USA.EP'; + if (lower.includes('expedited')) return 'DOM.EP'; + if (lower.includes('regular') && lower.includes('usa')) return 'USA.PW.ENV'; + if (lower.includes('regular')) return 'DOM.RP'; + if (lower.includes('small packet') && lower.includes('usa')) return 'USA.SP.AIR'; + if (lower.includes('small packet') && lower.includes('surface')) return 'INT.SP.SURF'; + if (lower.includes('small packet') && lower.includes('air')) return 'INT.SP.AIR'; + if (lower.includes('tracked packet') && lower.includes('usa')) return 'USA.TP'; + if (lower.includes('tracked packet')) return 'INT.TP'; + if (lower.includes('international') && lower.includes('parcel') && lower.includes('surface')) return 'INT.IP.SURF'; + if (lower.includes('international') && lower.includes('parcel') && lower.includes('air')) return 'INT.IP'; + if (lower.includes('surface') && lower.includes('international')) return 'INT.SP.SURF'; + if (lower.includes('air') && lower.includes('international')) return 'INT.SP.AIR'; + if (lower.includes('u.s.') || lower.includes('usa')) return 'USA.TP'; + if (lower.includes('international')) return 'INT.TP'; + return 'DOM.RP'; +} + +export interface RateOption { + serviceName: string; + serviceCode: string; + priceDetails: { base: number; gst: number; pst: number; hst: number; total: number }; + deliveryDate: string; + transitDays: string; + currency: string; +} + +interface LetterMailOption { + serviceName: string; + serviceCode: string; + priceDetails: { base: number; gst: number; pst: number; hst: number; total: number }; + deliveryDate: string; + transitDays: string; + isLettermail: boolean; + note: string; +} + +export function getLetterMailOptions( + weightGrams: number, + lengthCm: number, + widthCm: number, + heightCm: number, + country: string +): LetterMailOption[] { + const options: LetterMailOption[] = []; + + const lengthMm = lengthCm * 10; + const widthMm = widthCm * 10; + const heightMm = heightCm * 10; + + const meetsMinDimensions = lengthMm >= 140 && widthMm >= 90; + const isStandardSize = lengthMm <= 245 && widthMm <= 156 && heightMm <= 5; + const isOversizeSize = lengthMm <= 380 && widthMm <= 270 && heightMm <= 20; + + // Lettermail prices from Canada Post's published rate card (updated periodically) + if (meetsMinDimensions && isStandardSize && weightGrams <= 30 && weightGrams >= 2) { + let price: number; + if (country === 'CA') price = 1.75; + else if (country === 'US') price = 2.0; + else price = 3.5; + + const countryLabel = country === 'CA' ? 'Domestic' : country === 'US' ? 'USA' : 'International'; + options.push({ + serviceName: `Lettermail ${countryLabel} (up to 30g)`, + serviceCode: 'LETTERMAIL.STD', + priceDetails: { base: price, gst: 0, pst: 0, hst: 0, total: price }, + deliveryDate: 'N/A', + transitDays: country === 'CA' ? '2-4' : country === 'US' ? '4-7' : '7-14', + isLettermail: true, + note: 'Max: 245mm x 156mm x 5mm' + }); + } + + // Bubble packet prices from Canada Post's published rate card (updated periodically) + if (isOversizeSize && weightGrams >= 5 && weightGrams <= 500) { + let price: number; + const countryLabel = country === 'CA' ? 'Domestic' : country === 'US' ? 'USA' : 'International'; + + if (country === 'CA') { + if (weightGrams <= 100) price = 3.11; + else if (weightGrams <= 200) price = 4.51; + else if (weightGrams <= 300) price = 5.91; + else if (weightGrams <= 400) price = 6.62; + else price = 7.05; + } else if (country === 'US') { + if (weightGrams <= 100) price = 4.51; + else if (weightGrams <= 200) price = 7.16; + else price = 13.38; + } else { + if (weightGrams <= 100) price = 8.08; + else if (weightGrams <= 200) price = 13.38; + else price = 25.80; + } + + options.push({ + serviceName: `Bubble Packet ${countryLabel} (up to 500g)`, + serviceCode: 'BUBBLE.PACKET', + priceDetails: { base: price, gst: 0, pst: 0, hst: 0, total: price }, + deliveryDate: 'N/A', + transitDays: country === 'CA' ? '2-5' : country === 'US' ? '5-10' : '10-21', + isLettermail: true, + note: 'Max: 380mm x 270mm x 20mm' + }); + } + + return options; +} + +export function getCanadaPostConfig(): { baseUrl: string; authHeader: string; customerNumber: string } { + const username = env.CP_API_USERNAME; + const password = env.CP_API_PASSWORD; + const customerNumber = env.CP_CUSTOMER_NUMBER; + + if (!username || !password || !customerNumber) { + throw new Error('Canada Post API not configured (CP_API_USERNAME, CP_API_PASSWORD, CP_CUSTOMER_NUMBER required)'); + } + + const baseUrl = env.CP_ENVIRONMENT === 'production' + ? 'https://soa-gw.canadapost.ca' + : 'https://ct.soa-gw.canadapost.ca'; + + const authHeader = `Basic ${btoa(`${username}:${password}`)}`; + + return { baseUrl, authHeader, customerNumber }; +} + +interface PriceQuote { + 'service-name': string; + 'service-code': string; + 'price-details': { + base?: string; + due?: string; + taxes?: { + gst?: string | { $: string }; + pst?: string | { $: string }; + hst?: string | { $: string }; + }; + }; + 'service-standard'?: { + 'expected-delivery-date'?: string; + 'expected-transit-time'?: string; + }; +} + +function getTaxValue(tax: string | { $: string } | undefined): number { + if (!tax) return 0; + if (typeof tax === 'string') return parseFloat(tax) || 0; + return parseFloat(tax.$) || 0; +} + +export async function fetchRates(params: { + country: string; + postalCode?: string; + weightKg: number; + lengthCm: number; + widthCm: number; + heightCm: number; +}): Promise { + const { baseUrl, authHeader } = getCanadaPostConfig(); + const originPostal = env.CP_ORIGIN_POSTAL_CODE; + if (!originPostal) { + throw new Error('CP_ORIGIN_POSTAL_CODE is required'); + } + + const xmlBody = buildRateRequestXml({ + originPostal, + country: params.country, + postalCode: params.postalCode, + weightKg: params.weightKg, + lengthCm: params.lengthCm, + widthCm: params.widthCm, + heightCm: params.heightCm + }); + + const response = await fetch(`${baseUrl}/rs/ship/price`, { + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.cpc.ship.rate-v4+xml', + Accept: 'application/vnd.cpc.ship.rate-v4+xml', + Authorization: authHeader, + 'Accept-language': 'en-CA' + }, + body: xmlBody + }); + + const xmlResponse = await response.text(); + + if (!response.ok) { + console.error('Canada Post API error:', xmlResponse); + return []; + } + + const parser = new xml2js.Parser({ explicitArray: false }); + const result = await parser.parseStringPromise(xmlResponse); + const cadToUsd = await getCadToUsdRate(); + const handlingFee = 2.0; + + const priceQuotes = result['price-quotes'] as { 'price-quote'?: PriceQuote | PriceQuote[] } | undefined; + if (!priceQuotes?.['price-quote']) return []; + + let quotes = priceQuotes['price-quote']; + if (!Array.isArray(quotes)) quotes = [quotes]; + + return quotes.map((quote) => { + const priceDetails = quote['price-details']; + const taxes = priceDetails.taxes ?? {}; + const baseTotalCAD = parseFloat(priceDetails.due ?? '0'); + const totalCAD = baseTotalCAD + handlingFee; + const totalUSD = Math.round(totalCAD * cadToUsd * 100) / 100; + + return { + serviceName: quote['service-name'], + serviceCode: quote['service-code'], + priceDetails: { + base: Math.round(parseFloat(priceDetails.base ?? '0') * cadToUsd * 100) / 100, + gst: Math.round(getTaxValue(taxes.gst) * cadToUsd * 100) / 100, + pst: Math.round(getTaxValue(taxes.pst) * cadToUsd * 100) / 100, + hst: Math.round(getTaxValue(taxes.hst) * cadToUsd * 100) / 100, + total: totalUSD + }, + deliveryDate: quote['service-standard']?.['expected-delivery-date'] ?? 'N/A', + transitDays: quote['service-standard']?.['expected-transit-time'] ?? 'N/A', + currency: 'USD' + }; + }); +} + + +export async function createShipment(params: { + order: any; + weightKg: number; + lengthCm: number; + widthCm: number; + heightCm: number; + serviceCode: string; +}): Promise<{ trackingPin: string | null; labelBase64: string | null }> { + const { baseUrl, authHeader, customerNumber } = getCanadaPostConfig(); + + const shipmentXml = buildCreateShipmentXml(params); + + const contractId = env.CP_CONTRACT_ID; + const cpEndpoint = contractId + ? `${baseUrl}/rs/${customerNumber}/${customerNumber}/shipment` + : `${baseUrl}/rs/${customerNumber}/ncshipment`; + + // No retry loop: retrying shipment creation risks duplicate labels and double-charging carriers + const cpRes = await fetch(cpEndpoint, { + method: 'POST', + headers: { + 'Content-Type': contractId ? 'application/vnd.cpc.shipment-v8+xml' : 'application/vnd.cpc.ncshipment-v4+xml', + Accept: contractId ? 'application/vnd.cpc.shipment-v8+xml' : 'application/vnd.cpc.ncshipment-v4+xml', + Authorization: authHeader, + 'Accept-language': 'en-CA', + ...(params.order.country === 'US' && env.ZONOS_ACCOUNT_KEY ? { 'X-CPC-Zonos-Key': env.ZONOS_ACCOUNT_KEY } : {}) + }, + body: shipmentXml + }); + + if (!cpRes.ok) { + const errText = await cpRes.text(); + console.error('Canada Post Create Shipment error:', errText); + throw new Error(`Canada Post shipment creation failed: ${cpRes.status}`); + } + + const cpXml = await cpRes!.text(); + const parser = new xml2js.Parser({ explicitArray: false }); + const cpResult = await parser.parseStringPromise(cpXml); + const shipmentInfo = cpResult['shipment-info'] || cpResult['non-contract-shipment-info']; + + const trackingPin: string | null = shipmentInfo?.['tracking-pin'] || null; + let labelBase64: string | null = null; + + const links = shipmentInfo?.links?.link; + if (links) { + const linkArray = Array.isArray(links) ? links : [links]; + const labelLink = linkArray.find((l: any) => l.$?.rel === 'label'); + if (labelLink?.$?.href) { + const labelRes = await fetch(labelLink.$.href, { + headers: { + Accept: 'application/pdf', + Authorization: authHeader + } + }); + if (labelRes.ok) { + let labelBuffer: ArrayBuffer | Uint8Array = await labelRes.arrayBuffer(); + if (!contractId) { + labelBuffer = await cropLabelTo4x6(labelBuffer); + } + labelBase64 = `data:application/pdf;base64,${arrayBufferToBase64(labelBuffer)}`; + } + } + } + + return { trackingPin, labelBase64 }; +} + +export interface ZonosDutyResult { + duties: number; + taxes: number; + fees: number; + total: number; + currency: string; +} + +export async function calculateZonosDuties(params: { + items: Array<{ hsCode: string; valueCadCents: number; quantity: number; sku: string; description: string }>; + shippingCostCad: number; + destinationAddress: { city: string; state: string; postalCode: string; country: string }; + serviceLevelCode?: string; +}): Promise { + const credentialToken = env.ZONOS_CREDENTIAL_TOKEN; + if (!credentialToken) return null; + + const originPostal = (env.CP_ORIGIN_POSTAL_CODE || '').replace(/\s/g, '').toUpperCase(); + + const parties = [ + { + type: 'ORIGIN', + location: { + countryCode: 'CA', + postalCode: originPostal + } + }, + { + type: 'DESTINATION', + location: { + countryCode: params.destinationAddress.country, + administrativeArea: params.destinationAddress.state, + city: params.destinationAddress.city, + postalCode: params.destinationAddress.postalCode + } + } + ]; + + const items = params.items.map((item) => ({ + amount: item.valueCadCents / 100, + currencyCode: 'CAD', + quantity: item.quantity, + hsCode: item.hsCode || undefined, + sku: item.sku || undefined, + description: item.description, + countryOfOrigin: 'CA', + productId: item.sku || undefined + })); + + const shipmentRating = [ + { + amount: params.shippingCostCad, + currencyCode: 'CAD', + serviceLevelCode: params.serviceLevelCode || 'standard' + } + ]; + + const landedCostConfig = { + calculationMethod: 'DDP_PREFERRED', + endUse: 'NOT_FOR_RESALE', + tariffRate: 'ZONOS_PREFERRED' + }; + + const query = `mutation CalculateLandedCost($parties: [PartyCreateWorkflowInput!]!, $items: [ItemCreateWorkflowInput!]!, $shipmentRating: [ShipmentRatingCreateWorkflowInput!]!, $landedCostConfig: LandedCostWorkFlowInput!) { + partyCreateWorkflow(input: $parties) { id type } + itemCreateWorkflow(input: $items) { id amount } + shipmentRatingCreateWorkflow(input: $shipmentRating) { id amount } + landedCostCalculateWorkflow(input: $landedCostConfig) { id duties { amount currency } taxes { amount currency } fees { amount currency } } +}`; + + try { + const res = await fetch('https://api.zonos.com/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + credentialToken + }, + body: JSON.stringify({ + query, + variables: { parties, items, shipmentRating, landedCostConfig } + }) + }); + + if (!res.ok) { + console.error('Zonos API error:', res.status, await res.text()); + return null; + } + + const result = await res.json(); + + if (result.errors) { + console.error('Zonos GraphQL errors:', result.errors); + return null; + } + + const landedCost = result.data?.landedCostCalculateWorkflow; + if (!landedCost) return null; + + const sumAmounts = (arr: Array<{ amount: number }> | undefined) => + (arr || []).reduce((sum, entry) => sum + (entry.amount || 0), 0); + + const duties = sumAmounts(landedCost.duties); + const taxes = sumAmounts(landedCost.taxes); + const fees = sumAmounts(landedCost.fees); + + return { + duties: Math.round(duties * 100) / 100, + taxes: Math.round(taxes * 100) / 100, + fees: Math.round(fees * 100) / 100, + total: Math.round((duties + taxes + fees) * 100) / 100, + currency: 'USD' + }; + } catch (err) { + console.error('Zonos API request failed:', err); + return null; + } +} + +export async function fetchCheapestRate(params: { + country: string; + postalCode?: string; + province?: string; + weightGrams: number; + lengthIn: number; + widthIn: number; + heightIn: number; + packageType: string; +}): Promise<{ serviceName: string; shippingCostUsd: number } | null> { + const originPostal = env.CP_ORIGIN_POSTAL_CODE; + if (!originPostal || !env.CP_API_USERNAME || !env.CP_API_PASSWORD || !env.CP_CUSTOMER_NUMBER) { + return null; + } + + let effectiveLength = params.lengthIn; + let effectiveWidth = params.widthIn; + let effectivePackageType = params.packageType; + if (params.packageType === 'flat' || params.packageType === 'envelope') { + const l = Math.max(params.lengthIn, params.widthIn); + const w = Math.min(params.lengthIn, params.widthIn); + if (l <= 6 && w <= 4) { effectiveLength = 6; effectiveWidth = 4; } + else if (l <= 9 && w <= 6) { effectiveLength = 9; effectiveWidth = 6; } + else { effectiveLength = l; effectiveWidth = w; effectivePackageType = 'box'; } + } + + const lengthCm = inchesToCm(effectiveLength); + const widthCm = inchesToCm(effectiveWidth); + const heightCm = effectivePackageType === 'box' + ? inchesToCm(params.packageType === 'box' ? params.heightIn : 0.5) + : 0.5; + + const allOptions: Array<{ serviceName: string; total: number }> = []; + + const lettermailOpts = getLetterMailOptions(params.weightGrams, lengthCm, widthCm, heightCm, params.country); + for (const opt of lettermailOpts) { + allOptions.push({ serviceName: opt.serviceName, total: opt.priceDetails.total }); + } + + try { + const parcelRates = await fetchRates({ + country: params.country, + postalCode: params.postalCode, + weightKg: params.weightGrams * GRAMS_TO_KG, + lengthCm, + widthCm, + heightCm + }); + for (const rate of parcelRates) { + allOptions.push({ serviceName: rate.serviceName, total: rate.priceDetails.total }); + } + } catch (err) { + console.error('Parcel rate lookup failed:', err); + } + + try { + if (env.CHITCHATS_ACCESS_TOKEN && env.CHITCHATS_CLIENT_ID) { + const chitChatsRates = await fetchChitChatsRates({ + country: params.country, + postalCode: params.postalCode, + province: params.province, + name: 'Rate Quote', + address1: '123 Main St', + city: 'Unknown', + weightGrams: params.weightGrams, + lengthIn: effectiveLength, + widthIn: effectiveWidth, + heightIn: params.packageType === 'box' ? params.heightIn : 0.5, + valueCad: 1.00 + }); + for (const rate of chitChatsRates) { + allOptions.push({ serviceName: rate.serviceName, total: rate.priceDetails.total }); + } + } + } catch (err) { + console.error('Chit Chats rate lookup failed:', err); + } + + if (allOptions.length === 0) return null; + + allOptions.sort((a, b) => a.total - b.total); + return { serviceName: allOptions[0].serviceName, shippingCostUsd: allOptions[0].total }; +} diff --git a/resolution-frontend/src/lib/server/chit-chats.ts b/resolution-frontend/src/lib/server/chit-chats.ts new file mode 100644 index 0000000..5af5545 --- /dev/null +++ b/resolution-frontend/src/lib/server/chit-chats.ts @@ -0,0 +1,292 @@ +import { env } from '$env/dynamic/private'; +import type { RateOption } from './canada-post'; +import { getCadToUsdRate } from './exchange-rate'; +import { arrayBufferToBase64 } from './utils'; + +function formatHsCode(code: string | null | undefined): string { + if (!code) return '7117199000'; + const digits = code.replace(/[^0-9]/g, ''); + if (digits.length === 0) return '7117199000'; + if (digits.length > 10) return digits.substring(0, 10); + return digits; +} + + +export function getChitChatsConfig(): { baseUrl: string; accessToken: string; clientId: string } { + const accessToken = env.CHITCHATS_ACCESS_TOKEN; + const clientId = env.CHITCHATS_CLIENT_ID; + + if (!accessToken || !clientId) { + throw new Error('Chit Chats not configured: CHITCHATS_ACCESS_TOKEN and CHITCHATS_CLIENT_ID required'); + } + + return { + baseUrl: `https://chitchats.com/api/v1/clients/${clientId}`, + accessToken, + clientId + }; +} + +export async function createChitChatsShipment(params: { + order: any; + weightGrams: number; + lengthIn: number; + widthIn: number; + heightIn: number; +}): Promise<{ trackingNumber: string | null; labelBase64: string | null; shipmentId: string }> { + const { baseUrl, accessToken } = getChitChatsConfig(); + const { order, weightGrams, lengthIn, widthIn, heightIn } = params; + + const items = (order.items || []).filter((oi: any) => oi.warehouseItem); + const totalValue = items.length > 0 + ? items.reduce((sum: number, oi: any) => sum + (oi.warehouseItem.costCents / 100) * oi.quantity, 0).toFixed(2) + : '1.00'; + + const isThickEnvelope = lengthIn <= 15 && widthIn <= 10 && heightIn <= 1; + const packageType = isThickEnvelope ? 'thick_envelope' : 'parcel'; + + const shipmentBody: Record = { + name: `${order.firstName} ${order.lastName}`, + address_1: order.addressLine1, + address_2: order.addressLine2 || undefined, + city: order.city, + province_code: order.stateProvince, + postal_code: order.postalCode || '', + country_code: order.country, + email: order.email || undefined, + phone: order.phone || undefined, + package_contents: 'merchandise', + value: totalValue, + value_currency: 'cad', + package_type: packageType, + weight_unit: 'g', + weight: weightGrams, + size_unit: 'in', + size_x: lengthIn, + size_y: widthIn, + size_z: heightIn, + postage_type: 'unknown', + ship_date: 'today', + line_items: items.map((oi: any) => ({ + quantity: oi.quantity, + description: oi.warehouseItem.name, + value_amount: (oi.warehouseItem.costCents / 100).toFixed(2), + currency_code: 'cad', + origin_country: 'CA', + hs_tariff_code: formatHsCode(oi.warehouseItem.hsCode), + weight: oi.warehouseItem.weightGrams * oi.quantity, + weight_unit: 'g', + manufacturer_id: 'HACKCLUB', + manufacturer_contact: env.CP_SENDER_NAME || 'Hack Club', + manufacturer_street: env.CP_SENDER_ADDRESS || '', + manufacturer_city: env.CP_SENDER_CITY || '', + manufacturer_postal_code: (env.CP_ORIGIN_POSTAL_CODE || '').replace(/\s/g, ''), + manufacturer_province_code: env.CP_SENDER_PROVINCE || '', + manufacturer_country_code: 'CA' + })) + }; + + // Create shipment + const createRes = await fetch(`${baseUrl}/shipments`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken + }, + body: JSON.stringify(shipmentBody) + }); + + if (!createRes.ok) { + const errText = await createRes.text(); + console.error('Chit Chats Create Shipment error:', errText); + throw new Error(`Chit Chats shipment creation failed: ${createRes.status}`); + } + + const createData = await createRes.json(); + const shipment = createData.shipment || createData; + const shipmentId = shipment.id; + + // Find cheapest rate + const rates = shipment.rates || []; + if (rates.length === 0) { + throw new Error('No rates returned from Chit Chats'); + } + + const cheapestRate = rates.reduce((cheapest: any, rate: any) => { + return parseFloat(rate.payment_amount) < parseFloat(cheapest.payment_amount) ? rate : cheapest; + }, rates[0]); + + // Buy postage + const buyRes = await fetch(`${baseUrl}/shipments/${shipmentId}/buy`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken + }, + body: JSON.stringify({ postage_type: cheapestRate.postage_type }) + }); + + if (!buyRes.ok) { + const errText = await buyRes.text(); + console.error('Chit Chats Buy Postage error:', errText); + throw new Error(`Chit Chats postage purchase failed: ${buyRes.status}`); + } + + // Poll for ready status + let finalShipment: any = null; + for (let i = 0; i < 10; i++) { + await new Promise((r) => setTimeout(r, 1000)); + + const pollRes = await fetch(`${baseUrl}/shipments/${shipmentId}`, { + headers: { Authorization: accessToken } + }); + + if (!pollRes.ok) { + console.error('Chit Chats poll error:', pollRes.status); + continue; + } + + const pollData = await pollRes.json(); + finalShipment = pollData.shipment; + + if (finalShipment.status === 'ready' || finalShipment.status === 'postage_purchase_failed') { + break; + } + } + + if (!finalShipment || finalShipment.status === 'postage_purchase_failed') { + throw new Error('Chit Chats postage purchase failed'); + } + + // Fetch label PNG (4x6 format) + let labelBase64: string | null = null; + if (finalShipment.postage_label_png_url) { + try { + const labelRes = await fetch(finalShipment.postage_label_png_url); + if (labelRes.ok) { + const labelBuffer = await labelRes.arrayBuffer(); + labelBase64 = `data:image/png;base64,${arrayBufferToBase64(labelBuffer)}`; + } else { + console.error('Chit Chats label fetch failed:', labelRes.status); + } + } catch (err) { + console.error('Chit Chats label fetch error:', err); + } + } + + return { + trackingNumber: finalShipment.carrier_tracking_code || null, + labelBase64, + shipmentId: String(shipmentId) + }; +} + +export async function fetchChitChatsRates(params: { + country: string; + postalCode?: string; + province?: string; + name: string; + address1: string; + city: string; + weightGrams: number; + lengthIn: number; + widthIn: number; + heightIn: number; + valueCad: number; +}): Promise { + const { baseUrl, accessToken } = getChitChatsConfig(); + + const isThickEnvelope = params.lengthIn <= 15 && params.widthIn <= 10 && params.heightIn <= 1; + const packageType = isThickEnvelope ? 'thick_envelope' : 'parcel'; + + const shipmentBody: Record = { + name: params.name, + address_1: params.address1, + city: params.city, + province_code: params.province || '', + postal_code: params.postalCode || '', + country_code: params.country, + package_contents: 'merchandise', + value: params.valueCad.toFixed(2), + value_currency: 'cad', + package_type: packageType, + weight_unit: 'g', + weight: params.weightGrams, + size_unit: 'in', + size_x: params.lengthIn, + size_y: params.widthIn, + size_z: params.heightIn, + postage_type: 'unknown', + ship_date: 'today', + description: 'Merchandise', + line_items: [{ + quantity: 1, + description: 'Merchandise', + value_amount: params.valueCad.toFixed(2), + currency_code: 'cad', + origin_country: 'CA', + hs_tariff_code: '7117199000', + weight: params.weightGrams, + weight_unit: 'g', + manufacturer_id: 'HACKCLUB', + manufacturer_contact: env.CP_SENDER_NAME || 'Hack Club', + manufacturer_street: env.CP_SENDER_ADDRESS || '', + manufacturer_city: env.CP_SENDER_CITY || '', + manufacturer_postal_code: (env.CP_ORIGIN_POSTAL_CODE || '').replace(/\s/g, ''), + manufacturer_province_code: env.CP_SENDER_PROVINCE || '', + manufacturer_country_code: 'CA' + }] + }; + + const createRes = await fetch(`${baseUrl}/shipments`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken + }, + body: JSON.stringify(shipmentBody) + }); + + if (!createRes.ok) { + const errText = await createRes.text(); + console.error('Chit Chats rate fetch error:', errText); + throw new Error(`Chit Chats rate fetch failed: ${createRes.status}`); + } + + const createData = await createRes.json(); + const shipment = createData.shipment || createData; + const shipmentId = shipment.id; + const rates = shipment.rates || []; + + const cadToUsd = await getCadToUsdRate(); + + const rateOptions: RateOption[] = rates.map((rate: any) => { + const transitMatch = (rate.delivery_time_description || '').match(/(\d+)/); + return { + serviceName: rate.postage_description || rate.postage_type, + serviceCode: `CHITCHATS.${rate.postage_type}`, + priceDetails: { + base: parseFloat(rate.purchase_amount || '0') * cadToUsd, + gst: parseFloat(rate.federal_tax || '0') * cadToUsd, + pst: parseFloat(rate.provincial_tax || '0') * cadToUsd, + hst: 0, + total: parseFloat(rate.payment_amount || '0') * cadToUsd + }, + deliveryDate: 'N/A', + transitDays: transitMatch ? transitMatch[0] : 'N/A', + currency: 'USD' + }; + }); + + // Delete temporary shipment + try { + await fetch(`${baseUrl}/shipments/${shipmentId}`, { + method: 'DELETE', + headers: { Authorization: accessToken } + }); + } catch (err) { + console.error('Chit Chats cleanup error:', err); + } + + return rateOptions; +} diff --git a/resolution-frontend/src/lib/server/countries.ts b/resolution-frontend/src/lib/server/countries.ts new file mode 100644 index 0000000..9366839 --- /dev/null +++ b/resolution-frontend/src/lib/server/countries.ts @@ -0,0 +1,98 @@ +const COUNTRY_NAME_TO_CODE: Record = { + 'AFGHANISTAN': 'AF', 'ALBANIA': 'AL', 'ALGERIA': 'DZ', 'ANDORRA': 'AD', 'ANGOLA': 'AO', + 'ANTIGUA AND BARBUDA': 'AG', 'ARGENTINA': 'AR', 'ARMENIA': 'AM', 'AUSTRALIA': 'AU', + 'AUSTRIA': 'AT', 'AZERBAIJAN': 'AZ', 'BAHAMAS': 'BS', 'BAHRAIN': 'BH', 'BANGLADESH': 'BD', + 'BARBADOS': 'BB', 'BELARUS': 'BY', 'BELGIUM': 'BE', 'BELIZE': 'BZ', 'BENIN': 'BJ', + 'BHUTAN': 'BT', 'BOLIVIA': 'BO', 'BOSNIA AND HERZEGOVINA': 'BA', 'BOTSWANA': 'BW', + 'BRAZIL': 'BR', 'BRUNEI': 'BN', 'BULGARIA': 'BG', 'BURKINA FASO': 'BF', 'BURUNDI': 'BI', + 'CABO VERDE': 'CV', 'CAMBODIA': 'KH', 'CAMEROON': 'CM', 'CANADA': 'CA', + 'CENTRAL AFRICAN REPUBLIC': 'CF', 'CHAD': 'TD', 'CHILE': 'CL', 'CHINA': 'CN', + 'COLOMBIA': 'CO', 'COMOROS': 'KM', 'CONGO': 'CG', + 'DEMOCRATIC REPUBLIC OF THE CONGO': 'CD', 'COSTA RICA': 'CR', 'CROATIA': 'HR', 'CUBA': 'CU', + 'CYPRUS': 'CY', 'CZECH REPUBLIC': 'CZ', 'CZECHIA': 'CZ', 'DENMARK': 'DK', 'DJIBOUTI': 'DJ', + 'DOMINICA': 'DM', 'DOMINICAN REPUBLIC': 'DO', 'ECUADOR': 'EC', 'EGYPT': 'EG', + 'EL SALVADOR': 'SV', 'EQUATORIAL GUINEA': 'GQ', 'ERITREA': 'ER', 'ESTONIA': 'EE', + 'ESWATINI': 'SZ', 'ETHIOPIA': 'ET', 'FIJI': 'FJ', 'FINLAND': 'FI', 'FRANCE': 'FR', + 'GABON': 'GA', 'GAMBIA': 'GM', 'GEORGIA': 'GE', 'GERMANY': 'DE', 'GHANA': 'GH', + 'GREECE': 'GR', 'GRENADA': 'GD', 'GUATEMALA': 'GT', 'GUINEA': 'GN', 'GUINEA-BISSAU': 'GW', + 'GUYANA': 'GY', 'HAITI': 'HT', 'HONDURAS': 'HN', 'HUNGARY': 'HU', 'ICELAND': 'IS', + 'INDIA': 'IN', 'INDONESIA': 'ID', 'IRAN': 'IR', 'IRAQ': 'IQ', 'IRELAND': 'IE', + 'ISRAEL': 'IL', 'ITALY': 'IT', 'IVORY COAST': 'CI', "COTE D'IVOIRE": 'CI', + 'JAMAICA': 'JM', 'JAPAN': 'JP', 'JORDAN': 'JO', 'KAZAKHSTAN': 'KZ', 'KENYA': 'KE', + 'KIRIBATI': 'KI', 'SOUTH KOREA': 'KR', 'KOREA': 'KR', 'KUWAIT': 'KW', 'KYRGYZSTAN': 'KG', + 'LAOS': 'LA', 'LATVIA': 'LV', 'LEBANON': 'LB', 'LESOTHO': 'LS', 'LIBERIA': 'LR', + 'LIBYA': 'LY', 'LIECHTENSTEIN': 'LI', 'LITHUANIA': 'LT', 'LUXEMBOURG': 'LU', + 'MADAGASCAR': 'MG', 'MALAWI': 'MW', 'MALAYSIA': 'MY', 'MALDIVES': 'MV', 'MALI': 'ML', + 'MALTA': 'MT', 'MARSHALL ISLANDS': 'MH', 'MAURITANIA': 'MR', 'MAURITIUS': 'MU', + 'MEXICO': 'MX', 'MÉXICO': 'MX', 'MICRONESIA': 'FM', 'MOLDOVA': 'MD', 'MONACO': 'MC', + 'MONGOLIA': 'MN', 'MONTENEGRO': 'ME', 'MOROCCO': 'MA', 'MOZAMBIQUE': 'MZ', 'MYANMAR': 'MM', + 'NAMIBIA': 'NA', 'NAURU': 'NR', 'NEPAL': 'NP', 'NETHERLANDS': 'NL', 'NEW ZEALAND': 'NZ', + 'NICARAGUA': 'NI', 'NIGER': 'NE', 'NIGERIA': 'NG', 'NORTH MACEDONIA': 'MK', + 'NORWAY': 'NO', 'OMAN': 'OM', 'PAKISTAN': 'PK', 'PALAU': 'PW', 'PALESTINE': 'PS', + 'PANAMA': 'PA', 'PAPUA NEW GUINEA': 'PG', 'PARAGUAY': 'PY', 'PERU': 'PE', + 'PHILIPPINES': 'PH', 'POLAND': 'PL', 'PORTUGAL': 'PT', 'QATAR': 'QA', 'ROMANIA': 'RO', + 'RUSSIA': 'RU', 'RUSSIAN FEDERATION': 'RU', 'RWANDA': 'RW', + 'SAINT KITTS AND NEVIS': 'KN', 'SAINT LUCIA': 'LC', 'SAINT VINCENT AND THE GRENADINES': 'VC', + 'SAMOA': 'WS', 'SAN MARINO': 'SM', 'SAO TOME AND PRINCIPE': 'ST', 'SAUDI ARABIA': 'SA', + 'SENEGAL': 'SN', 'SERBIA': 'RS', 'SEYCHELLES': 'SC', 'SIERRA LEONE': 'SL', + 'SINGAPORE': 'SG', 'SLOVAKIA': 'SK', 'SLOVENIA': 'SI', 'SOLOMON ISLANDS': 'SB', + 'SOMALIA': 'SO', 'SOUTH AFRICA': 'ZA', 'SOUTH SUDAN': 'SS', 'SPAIN': 'ES', + 'SRI LANKA': 'LK', 'SUDAN': 'SD', 'SURINAME': 'SR', 'SWEDEN': 'SE', 'SWITZERLAND': 'CH', + 'SYRIA': 'SY', 'TAIWAN': 'TW', 'TAJIKISTAN': 'TJ', 'TANZANIA': 'TZ', 'THAILAND': 'TH', + 'TIMOR-LESTE': 'TL', 'TOGO': 'TG', 'TONGA': 'TO', 'TRINIDAD AND TOBAGO': 'TT', + 'TUNISIA': 'TN', 'TURKEY': 'TR', 'TÜRKIYE': 'TR', 'TURKMENISTAN': 'TM', 'TUVALU': 'TV', + 'UGANDA': 'UG', 'UKRAINE': 'UA', 'UNITED ARAB EMIRATES': 'AE', 'UAE': 'AE', + 'UNITED KINGDOM': 'GB', 'UK': 'GB', 'GREAT BRITAIN': 'GB', 'ENGLAND': 'GB', + 'UNITED STATES': 'US', 'UNITED STATES OF AMERICA': 'US', 'USA': 'US', + 'URUGUAY': 'UY', 'UZBEKISTAN': 'UZ', 'VANUATU': 'VU', 'VATICAN CITY': 'VA', + 'VENEZUELA': 'VE', 'VIETNAM': 'VN', 'VIET NAM': 'VN', 'YEMEN': 'YE', 'ZAMBIA': 'ZM', + 'ZIMBABWE': 'ZW', + 'HONG KONG': 'HK', 'MACAU': 'MO', 'PUERTO RICO': 'PR', 'GUAM': 'GU', + 'US VIRGIN ISLANDS': 'VI', 'AMERICAN SAMOA': 'AS', +}; + +/** + * Resolves a country value to an ISO 3166-1 alpha-2 code. + * Accepts either a 2-letter code (passed through) or a full country name. + * Returns the input uppercased if no mapping is found. + */ +export function resolveCountryCode(country: string): string { + const upper = country.trim().toUpperCase(); + if (upper.length === 2) return upper; + return COUNTRY_NAME_TO_CODE[upper] ?? upper; +} + +const US_STATE_TO_CODE: Record = { + 'ALABAMA': 'AL', 'ALASKA': 'AK', 'ARIZONA': 'AZ', 'ARKANSAS': 'AR', 'CALIFORNIA': 'CA', + 'COLORADO': 'CO', 'CONNECTICUT': 'CT', 'DELAWARE': 'DE', 'FLORIDA': 'FL', 'GEORGIA': 'GA', + 'HAWAII': 'HI', 'IDAHO': 'ID', 'ILLINOIS': 'IL', 'INDIANA': 'IN', 'IOWA': 'IA', + 'KANSAS': 'KS', 'KENTUCKY': 'KY', 'LOUISIANA': 'LA', 'MAINE': 'ME', 'MARYLAND': 'MD', + 'MASSACHUSETTS': 'MA', 'MICHIGAN': 'MI', 'MINNESOTA': 'MN', 'MISSISSIPPI': 'MS', + 'MISSOURI': 'MO', 'MONTANA': 'MT', 'NEBRASKA': 'NE', 'NEVADA': 'NV', 'NEW HAMPSHIRE': 'NH', + 'NEW JERSEY': 'NJ', 'NEW MEXICO': 'NM', 'NEW YORK': 'NY', 'NORTH CAROLINA': 'NC', + 'NORTH DAKOTA': 'ND', 'OHIO': 'OH', 'OKLAHOMA': 'OK', 'OREGON': 'OR', 'PENNSYLVANIA': 'PA', + 'RHODE ISLAND': 'RI', 'SOUTH CAROLINA': 'SC', 'SOUTH DAKOTA': 'SD', 'TENNESSEE': 'TN', + 'TEXAS': 'TX', 'UTAH': 'UT', 'VERMONT': 'VT', 'VIRGINIA': 'VA', 'WASHINGTON': 'WA', + 'WEST VIRGINIA': 'WV', 'WISCONSIN': 'WI', 'WYOMING': 'WY', + 'DISTRICT OF COLUMBIA': 'DC', 'PUERTO RICO': 'PR', 'GUAM': 'GU', + 'AMERICAN SAMOA': 'AS', 'US VIRGIN ISLANDS': 'VI', +}; + +const CA_PROVINCE_TO_CODE: Record = { + 'ALBERTA': 'AB', 'BRITISH COLUMBIA': 'BC', 'MANITOBA': 'MB', 'NEW BRUNSWICK': 'NB', + 'NEWFOUNDLAND AND LABRADOR': 'NL', 'NEWFOUNDLAND': 'NL', 'NOVA SCOTIA': 'NS', + 'NORTHWEST TERRITORIES': 'NT', 'NUNAVUT': 'NU', 'ONTARIO': 'ON', + 'PRINCE EDWARD ISLAND': 'PE', 'QUEBEC': 'QC', 'QUÉBEC': 'QC', + 'SASKATCHEWAN': 'SK', 'YUKON': 'YT', +}; + +/** + * Resolves a state/province value to a short code. + * Accepts a 2-letter code (passed through) or full name for US states / CA provinces. + */ +export function resolveStateCode(state: string, country?: string): string { + const trimmed = state.trim(); + if (trimmed.length <= 2) return trimmed.toUpperCase(); + const upper = trimmed.toUpperCase(); + return US_STATE_TO_CODE[upper] ?? CA_PROVINCE_TO_CODE[upper] ?? trimmed; +} diff --git a/resolution-frontend/src/lib/server/db/schema.ts b/resolution-frontend/src/lib/server/db/schema.ts index c31c9b9..bb5a87e 100644 --- a/resolution-frontend/src/lib/server/db/schema.ts +++ b/resolution-frontend/src/lib/server/db/schema.ts @@ -8,7 +8,9 @@ export const enrollmentStatusEnum = pgEnum('enrollment_status', ['ACTIVE', 'DROP export const pathwayEnum = pgEnum('pathway', ['PYTHON', 'RUST', 'GAME_DEV', 'HARDWARE', 'DESIGN', 'GENERAL_CODING']); export const difficultyEnum = pgEnum('difficulty', ['BEGINNER', 'INTERMEDIATE', 'ADVANCED']); export const shipStatusEnum = pgEnum('ship_status', ['PLANNED', 'IN_PROGRESS', 'SHIPPED', 'MISSED']); -export const payoutStatusEnum = pgEnum('payout_status', ['DRAFT', 'PENDING', 'PAID', 'CANCELED']); +export const payoutStatusEnum = pgEnum('payout_status', ['DRAFT', 'PENDING', 'PAID', 'CANCELLED']); +export const warehouseOrderStatusEnum = pgEnum('warehouse_order_status', ['DRAFT', 'ESTIMATED', 'APPROVED', 'SHIPPED', 'CANCELLED']); +export const warehouseBatchStatusEnum = pgEnum('warehouse_batch_status', ['AWAITING_MAPPING', 'MAPPED', 'PROCESSED']); // Tables export const user = pgTable('user', { @@ -149,6 +151,7 @@ export const userRelations = relations(user, ({ many }) => ({ weeklyShips: many(weeklyShip), payouts: many(ambassadorPayout), referralLinks: many(referralLink), + warehouseOrders: many(warehouseOrder), reviewerAssignments: many(reviewerPathway) })); @@ -288,3 +291,162 @@ export const referralSignupRelations = relations(referralSignup, ({ one }) => ({ referralLink: one(referralLink, { fields: [referralSignup.referralLinkId], references: [referralLink.id] }), user: one(user, { fields: [referralSignup.userId], references: [user.id] }) })); + +// Warehouse categories +export const warehouseCategory = pgTable('warehouse_category', { + id: text('id').primaryKey().$defaultFn(() => createId()), + name: text('name').notNull(), + sortOrder: integer('sort_order').notNull().default(0), + createdAt: timestamp('created_at', { mode: 'date' }).notNull().defaultNow() +}); + +// Warehouse items - inventory managed by admins +export const warehouseItem = pgTable('warehouse_item', { + id: text('id').primaryKey().$defaultFn(() => createId()), + categoryId: text('category_id').references(() => warehouseCategory.id, { onDelete: 'set null' }), + name: text('name').notNull(), + sku: text('sku').notNull().unique(), + sizing: text('sizing'), + packageType: text('package_type').notNull().default('box'), + lengthIn: real('length_in').notNull(), + widthIn: real('width_in').notNull(), + heightIn: real('height_in').notNull(), + weightGrams: real('weight_grams').notNull(), + costCents: integer('cost_cents').notNull(), + hsCode: text('hs_code').notNull().default(''), + quantity: integer('quantity').notNull().default(0), + imageUrl: text('image_url'), + createdAt: timestamp('created_at', { mode: 'date' }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { mode: 'date' }).notNull().defaultNow() +}); + +// Warehouse orders +export const warehouseOrder = pgTable('warehouse_order', { + id: text('id').primaryKey().$defaultFn(() => createId()), + fulfillmentId: integer('fulfillment_id').generatedAlwaysAsIdentity().unique(), + createdById: text('created_by_id').notNull().references(() => user.id, { onDelete: 'cascade' }), + batchId: text('batch_id'), + status: warehouseOrderStatusEnum('status').notNull().default('DRAFT'), + firstName: text('first_name').notNull(), + lastName: text('last_name').notNull(), + email: text('email').notNull(), + phone: text('phone'), + addressLine1: text('address_line_1').notNull(), + addressLine2: text('address_line_2'), + city: text('city').notNull(), + stateProvince: text('state_province').notNull(), + postalCode: text('postal_code'), + country: text('country').notNull(), + estimatedShippingCents: integer('estimated_shipping_cents'), + estimatedDutiesCents: integer('estimated_duties_cents'), + estimatedServiceName: text('estimated_service_name'), + estimatedServiceCode: text('estimated_service_code'), + estimatedPackageType: text('estimated_package_type'), + estimatedTotalLengthIn: real('estimated_total_length_in'), + estimatedTotalWidthIn: real('estimated_total_width_in'), + estimatedTotalHeightIn: real('estimated_total_height_in'), + estimatedTotalWeightGrams: real('estimated_total_weight_grams'), + trackingNumber: text('tracking_number'), + labelUrl: text('label_url'), + // Nullable because DRAFT orders don't have a shipping method yet; set when label is created + shippingMethod: text('shipping_method'), // 'canada_post', 'lettermail', or 'chitchats' + notes: text('notes'), + createdAt: timestamp('created_at', { mode: 'date' }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { mode: 'date' }).notNull().defaultNow() +}); + +// Warehouse order line items +export const warehouseOrderItem = pgTable('warehouse_order_item', { + id: text('id').primaryKey().$defaultFn(() => createId()), + orderId: text('order_id').notNull().references(() => warehouseOrder.id, { onDelete: 'cascade' }), + warehouseItemId: text('warehouse_item_id').notNull().references(() => warehouseItem.id, { onDelete: 'restrict' }), + quantity: integer('quantity').notNull().default(1), + // Selected size variant (e.g. 'S', 'M', 'L') when the warehouse item has sizing options + sizingChoice: text('sizing_choice') +}); + +// Warehouse order tags for filtering +export const warehouseOrderTag = pgTable('warehouse_order_tag', { + id: text('id').primaryKey().$defaultFn(() => createId()), + orderId: text('order_id').notNull().references(() => warehouseOrder.id, { onDelete: 'cascade' }), + tag: text('tag').notNull() +}, (table) => [ + uniqueIndex('warehouse_order_tag_unique_idx').on(table.orderId, table.tag), + index('warehouse_order_tag_tag_idx').on(table.tag) +]); + +export const warehouseOrderRelations = relations(warehouseOrder, ({ one, many }) => ({ + createdBy: one(user, { fields: [warehouseOrder.createdById], references: [user.id] }), + items: many(warehouseOrderItem), + tags: many(warehouseOrderTag) +})); + +export const warehouseOrderItemRelations = relations(warehouseOrderItem, ({ one }) => ({ + order: one(warehouseOrder, { fields: [warehouseOrderItem.orderId], references: [warehouseOrder.id] }), + warehouseItem: one(warehouseItem, { fields: [warehouseOrderItem.warehouseItemId], references: [warehouseItem.id] }) +})); + +export const warehouseOrderTagRelations = relations(warehouseOrderTag, ({ one }) => ({ + order: one(warehouseOrder, { fields: [warehouseOrderTag.orderId], references: [warehouseOrder.id] }) +})); + +// Order templates — reusable item lists for quickly creating warehouse orders (e.g. "Sticker Pack") +// isPublic controls whether other ambassadors can see and use this template +export const warehouseOrderTemplate = pgTable('warehouse_order_template', { + id: text('id').primaryKey().$defaultFn(() => createId()), + createdById: text('created_by_id').notNull().references(() => user.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + isPublic: boolean('is_public').notNull().default(false), + createdAt: timestamp('created_at', { mode: 'date' }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { mode: 'date' }).notNull().defaultNow() +}); + +export const warehouseOrderTemplateItem = pgTable('warehouse_order_template_item', { + id: text('id').primaryKey().$defaultFn(() => createId()), + templateId: text('template_id').notNull().references(() => warehouseOrderTemplate.id, { onDelete: 'cascade' }), + warehouseItemId: text('warehouse_item_id').notNull().references(() => warehouseItem.id, { onDelete: 'restrict' }), + quantity: integer('quantity').notNull().default(1) +}); + +export const warehouseOrderTemplateRelations = relations(warehouseOrderTemplate, ({ one, many }) => ({ + createdBy: one(user, { fields: [warehouseOrderTemplate.createdById], references: [user.id] }), + items: many(warehouseOrderTemplateItem) +})); + +export const warehouseOrderTemplateItemRelations = relations(warehouseOrderTemplateItem, ({ one }) => ({ + template: one(warehouseOrderTemplate, { fields: [warehouseOrderTemplateItem.templateId], references: [warehouseOrderTemplate.id] }), + warehouseItem: one(warehouseItem, { fields: [warehouseOrderTemplateItem.warehouseItemId], references: [warehouseItem.id] }) +})); + +// Batches +export const warehouseBatch = pgTable('warehouse_batch', { + id: text('id').primaryKey().$defaultFn(() => createId()), + createdById: text('created_by_id').notNull().references(() => user.id, { onDelete: 'cascade' }), + templateId: text('template_id').notNull().references(() => warehouseOrderTemplate.id, { onDelete: 'restrict' }), + title: text('title'), + status: warehouseBatchStatusEnum('status').notNull().default('AWAITING_MAPPING'), + csvData: text('csv_data').notNull(), + fieldMapping: text('field_mapping'), + addressCount: integer('address_count').notNull().default(0), + createdAt: timestamp('created_at', { mode: 'date' }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { mode: 'date' }).notNull().defaultNow() +}); + +export const warehouseBatchTag = pgTable('warehouse_batch_tag', { + id: text('id').primaryKey().$defaultFn(() => createId()), + batchId: text('batch_id').notNull().references(() => warehouseBatch.id, { onDelete: 'cascade' }), + tag: text('tag').notNull() +}, (table) => [ + uniqueIndex('warehouse_batch_tag_unique_idx').on(table.batchId, table.tag) +]); + +export const warehouseBatchRelations = relations(warehouseBatch, ({ one, many }) => ({ + createdBy: one(user, { fields: [warehouseBatch.createdById], references: [user.id] }), + template: one(warehouseOrderTemplate, { fields: [warehouseBatch.templateId], references: [warehouseOrderTemplate.id] }), + tags: many(warehouseBatchTag) +})); + +export const warehouseBatchTagRelations = relations(warehouseBatchTag, ({ one }) => ({ + batch: one(warehouseBatch, { fields: [warehouseBatchTag.batchId], references: [warehouseBatch.id] }) +})); + diff --git a/resolution-frontend/src/lib/server/exchange-rate.ts b/resolution-frontend/src/lib/server/exchange-rate.ts new file mode 100644 index 0000000..b8a9152 --- /dev/null +++ b/resolution-frontend/src/lib/server/exchange-rate.ts @@ -0,0 +1,43 @@ +import { env } from '$env/dynamic/private'; + +const CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour + +let cachedRate: number | null = null; +let cachedAt = 0; + +const FALLBACK_CAD_TO_USD = 0.73; + +/** + * Returns the current CAD to USD exchange rate. + * Fetches from an external API and caches for 1 hour. + * Falls back to a static rate if the API is unavailable. + */ +export async function getCadToUsdRate(): Promise { + if (cachedRate && Date.now() - cachedAt < CACHE_DURATION_MS) { + return cachedRate; + } + + try { + const res = await fetch('https://open.er-api.com/v6/latest/CAD', { + signal: AbortSignal.timeout(5000) + }); + + if (res.ok) { + const data = await res.json(); + const rate = data?.rates?.USD; + if (typeof rate === 'number' && rate > 0) { + cachedRate = Math.round(rate * 10000) / 10000; + cachedAt = Date.now(); + return cachedRate; + } + } + } catch (err) { + console.error('Exchange rate fetch failed, using fallback:', err); + } + + // Use env override or fallback + const envRate = env.CAD_TO_USD_RATE ? parseFloat(env.CAD_TO_USD_RATE) : null; + if (envRate && envRate > 0) return envRate; + + return FALLBACK_CAD_TO_USD; +} diff --git a/resolution-frontend/src/lib/server/hcb.ts b/resolution-frontend/src/lib/server/hcb.ts new file mode 100644 index 0000000..bc281a1 --- /dev/null +++ b/resolution-frontend/src/lib/server/hcb.ts @@ -0,0 +1,98 @@ +import { env } from '$env/dynamic/private'; + +const HCB_BASE_URL = 'https://hcb.hackclub.com/api/v4'; +const HCB_TOKEN_URL = 'https://hcb.hackclub.com/api/v4/oauth/token'; +const DEFAULT_REFRESH_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes fallback + +const RESOLUTION_ORG_ID = 'org_0zuxO2'; + +const PATHWAY_ORG_MAP: Record = { + PYTHON: 'org_lbu4GE', + GENERAL_CODING: 'org_5GuRj1', + GAME_DEV: 'org_NOuVO8', + DESIGN: 'org_Q4uqwO', + RUST: 'org_G3uEbg', + HARDWARE: 'org_0zuxZA' +}; + +let cachedAccessToken: string | null = null; +let cachedRefreshToken: string | null = null; +let tokenExpiresAt = 0; + +async function getAccessToken(): Promise { + if (cachedAccessToken && Date.now() < tokenExpiresAt) { + return cachedAccessToken; + } + + const clientId = env.HCB_CLIENT_ID; + const clientSecret = env.HCB_CLIENT_SECRET; + const refreshToken = cachedRefreshToken || env.HCB_REFRESH_TOKEN; + if (!clientId || !clientSecret || !refreshToken) { + throw new Error('HCB not configured: HCB_CLIENT_ID, HCB_CLIENT_SECRET, and HCB_REFRESH_TOKEN required'); + } + + // Use initial access token on first call if not yet refreshed + if (!cachedAccessToken && env.HCB_ACCESS_TOKEN) { + cachedAccessToken = env.HCB_ACCESS_TOKEN; + cachedRefreshToken = refreshToken; + tokenExpiresAt = Date.now() + DEFAULT_REFRESH_INTERVAL_MS; + return cachedAccessToken; + } + + const res = await fetch(HCB_TOKEN_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: clientId, + client_secret: clientSecret + }) + }); + + if (!res.ok) { + const body = await res.text(); + throw new Error(`HCB token refresh failed (${res.status}): ${body}`); + } + + const data = await res.json(); + cachedAccessToken = data.access_token; + cachedRefreshToken = data.refresh_token; + // Use expires_in from OAuth response if available, otherwise fall back to default + const expiresInMs = data.expires_in ? data.expires_in * 1000 : DEFAULT_REFRESH_INTERVAL_MS; + tokenExpiresAt = Date.now() + expiresInMs; + + return cachedAccessToken!; +} + +export async function createHcbTransfer( + fromOrgId: string, + amountCents: number, + memo: string +): Promise<{ id: string; amount_cents: number; name: string }> { + const accessToken = await getAccessToken(); + + const res = await fetch(`${HCB_BASE_URL}/organizations/${fromOrgId}/transfers`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + to_organization_id: RESOLUTION_ORG_ID, + amount_cents: amountCents, + name: memo + }) + }); + + if (!res.ok) { + const body = await res.text(); + throw new Error(`HCB transfer failed (${res.status}): ${body}`); + } + + return res.json(); +} + +export function getOrgIdForPathway(pathway: string): string | null { + return PATHWAY_ORG_MAP[pathway] ?? null; +} diff --git a/resolution-frontend/src/lib/server/utils.ts b/resolution-frontend/src/lib/server/utils.ts new file mode 100644 index 0000000..c1806d4 --- /dev/null +++ b/resolution-frontend/src/lib/server/utils.ts @@ -0,0 +1,4 @@ +/** Convert an ArrayBuffer or Uint8Array to a base64 string */ +export function arrayBufferToBase64(buffer: ArrayBuffer | Uint8Array): string { + return Buffer.from(buffer).toString('base64'); +} diff --git a/resolution-frontend/src/lib/server/validation/schemas.test.ts b/resolution-frontend/src/lib/server/validation/schemas.test.ts index 73e04a7..59ced9a 100644 --- a/resolution-frontend/src/lib/server/validation/schemas.test.ts +++ b/resolution-frontend/src/lib/server/validation/schemas.test.ts @@ -4,6 +4,7 @@ import { markShippedSchema, updateShipStatusSchema, enrollSeasonSchema, + shippingRateSchema, projectSubmissionSchema } from './schemas'; @@ -109,6 +110,71 @@ describe('enrollSeasonSchema', () => { }); }); +describe('shippingRateSchema', () => { + const validBox = { + country: 'US', + street: '123 Main St', + city: 'Springfield', + province: 'IL', + postalCode: '62701', + weight: 2.5, + packageType: 'box' as const, + length: 10, + width: 5, + height: 3 + }; + + const validEnvelope = { + country: 'US', + street: '123 Main St', + city: 'Springfield', + province: 'IL', + weight: 0.5, + packageType: 'envelope' as const, + length: 10, + width: 5 + }; + + it('accepts valid box input', () => { + expect(shippingRateSchema.safeParse(validBox).success).toBe(true); + }); + + it('accepts valid envelope input', () => { + expect(shippingRateSchema.safeParse(validEnvelope).success).toBe(true); + }); + + it('uppercases country code', () => { + const result = shippingRateSchema.safeParse({ ...validBox, country: 'us' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.country).toBe('US'); + } + }); + + it('rejects country code not exactly 2 chars', () => { + expect(shippingRateSchema.safeParse({ ...validBox, country: 'USA' }).success).toBe(false); + }); + + it('rejects non-positive weight', () => { + expect(shippingRateSchema.safeParse({ ...validBox, weight: 0 }).success).toBe(false); + expect(shippingRateSchema.safeParse({ ...validBox, weight: -1 }).success).toBe(false); + }); + + it('rejects box without height', () => { + const { height, ...noHeight } = validBox; + expect(shippingRateSchema.safeParse(noHeight).success).toBe(false); + }); + + it('rejects empty street', () => { + expect(shippingRateSchema.safeParse({ ...validBox, street: '' }).success).toBe(false); + }); + + it('allows optional postalCode', () => { + const { postalCode, ...noPostal } = validBox; + expect(shippingRateSchema.safeParse(noPostal).success).toBe(true); + }); +}); + describe('projectSubmissionSchema', () => { const valid = { codeUrl: 'https://github.com/user/repo', @@ -284,5 +350,3 @@ describe('projectSubmissionSchema', () => { expect(projectSubmissionSchema.safeParse({ ...valid, addressLine2: 'a'.repeat(201) }).success).toBe(false); }); }); - - diff --git a/resolution-frontend/src/lib/server/validation/schemas.ts b/resolution-frontend/src/lib/server/validation/schemas.ts index f87bf23..1a655e7 100644 --- a/resolution-frontend/src/lib/server/validation/schemas.ts +++ b/resolution-frontend/src/lib/server/validation/schemas.ts @@ -36,6 +36,43 @@ export const workshopIdSchema = z.object({ workshopId: z.string().min(1, 'Workshop ID is required') }); +const envelopeSchema = z.object({ + packageType: z.literal('envelope'), + length: z.number().positive('Length must be positive'), + width: z.number().positive('Width must be positive') +}); + +const flatSchema = z.object({ + packageType: z.literal('flat'), + length: z.number().positive('Length must be positive'), + width: z.number().positive('Width must be positive') +}); + +const boxSchema = z.object({ + packageType: z.literal('box'), + length: z.number().positive('Length must be positive'), + width: z.number().positive('Width must be positive'), + height: z.number().positive('Height must be positive') +}); + +export const shippingRateSchema = z.object({ + country: z.string().length(2, 'Country must be a 2-letter ISO code').toUpperCase(), + street: z.string().min(1, 'Street is required'), + city: z.string().min(1, 'City is required'), + province: z.string().min(1, 'Province/State is required'), + postalCode: z.string().optional(), + weight: z.number().positive('Weight must be positive'), + items: z.array(z.object({ + name: z.string(), + sku: z.string().optional(), + hsCode: z.string().optional(), + costCents: z.number(), + quantity: z.number().int().positive() + })).optional() +}).and(z.discriminatedUnion('packageType', [envelopeSchema, flatSchema, boxSchema])); + +export type ShippingRateInput = z.infer; + const safeUrl = z.string().url('Please enter a valid URL').max(2000).refine( (val) => /^https?:\/\//i.test(val), { message: 'URL must use http or https' } diff --git a/resolution-frontend/src/routes/api/fulfillment/get-label/+server.ts b/resolution-frontend/src/routes/api/fulfillment/get-label/+server.ts new file mode 100644 index 0000000..7a6d7f7 --- /dev/null +++ b/resolution-frontend/src/routes/api/fulfillment/get-label/+server.ts @@ -0,0 +1,328 @@ +import { env } from '$env/dynamic/private'; +import { json, error } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { warehouseOrder, ambassadorPathway } from '$lib/server/db/schema'; +import { eq, and, ne } from 'drizzle-orm'; +import type { RequestHandler } from './$types'; +import { requireAuth } from '$lib/server/auth/guard'; +import { GRAMS_TO_KG, inchesToCm, isLettermail, getServiceCode, createShipment } from '$lib/server/canada-post'; +import { createChitChatsShipment } from '$lib/server/chit-chats'; +import { arrayBufferToBase64 } from '$lib/server/utils'; + +function buildPackingSlipBase64(order: any): string { + const lines: string[] = [ + `PACKING SLIP`, + `Order #${order.fulfillmentId}`, + `Date: ${new Date().toLocaleDateString('en-US')}`, + ``, + `SHIP TO:`, + `${order.firstName} ${order.lastName}`, + `${order.addressLine1}`, + order.addressLine2 || '', + `${order.city}, ${order.stateProvince} ${order.postalCode || ''}`, + `${order.country}`, + ``, + `CONTENTS:`, + `${'Item'.padEnd(35)} ${'Size'.padEnd(10)} ${'Qty'.padEnd(5)}`, + `${'─'.repeat(50)}`, + ]; + for (const oi of order.items) { + const name = oi.warehouseItem.name.substring(0, 35).padEnd(35); + const size = (oi.sizingChoice || '—').padEnd(10); + const qty = String(oi.quantity).padEnd(5); + lines.push(`${name} ${size} ${qty}`); + } + lines.push(`${'─'.repeat(50)}`); + lines.push(`Total items: ${order.items.reduce((s: number, oi: any) => s + oi.quantity, 0)}`); + if (order.notes) { + lines.push(``); + lines.push(`NOTES: ${order.notes}`); + } + const text = lines.filter(l => l !== undefined).join('\n'); + return btoa(unescape(encodeURIComponent(text))); +} + +export const POST: RequestHandler = async (event) => { + const { user } = requireAuth(event); + + const isAmbassador = await db + .select({ userId: ambassadorPathway.userId }) + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, user.id)) + .limit(1); + + if (!user.isAdmin && isAmbassador.length === 0) { + throw error(403, 'Access denied - admin or ambassador only'); + } + + let body: any; + try { + body = await event.request.json(); + } catch { + throw error(400, 'Invalid JSON body'); + } + const orderId = body?.orderId; + const carrier = body?.carrier; + if (!orderId || typeof orderId !== 'string') throw error(400, 'Order ID required'); + if (carrier && !['auto', 'canada_post', 'chitchats', 'lettermail'].includes(carrier)) { + throw error(400, 'Invalid carrier'); + } + + const order = await db.query.warehouseOrder.findFirst({ + where: eq(warehouseOrder.id, orderId), + with: { + items: { + with: { + warehouseItem: true + } + } + } + }); + + if (!order) throw error(404, 'Order not found'); + + if (!user.isAdmin && order.createdById !== user.id) { + throw error(403, 'Access denied - you do not own this order'); + } + + // If the order already has a label, re-fetch and return it as base64 + if (order.labelUrl) { + let labelUrl = order.labelUrl; + if (!labelUrl.startsWith('data:')) { + try { + const parsed = new URL(labelUrl); + if (!['http:', 'https:'].includes(parsed.protocol)) { + throw error(400, 'Invalid label URL protocol'); + } + } catch (e: any) { + if (e?.status) throw e; + throw error(400, 'Invalid label URL'); + } + const labelRes = await fetch(labelUrl); + console.log('Label fetch status:', labelRes.status, labelRes.statusText); + if (labelRes.ok) { + const labelBuffer = await labelRes.arrayBuffer(); + console.log('Label PDF size:', labelBuffer.byteLength, 'bytes'); + labelUrl = `data:application/pdf;base64,${arrayBufferToBase64(labelBuffer)}`; + } else { + const errBody = await labelRes.text(); + console.error('Label fetch failed:', errBody); + throw error(502, `Failed to fetch label PDF: ${labelRes.status}`); + } + } + return json({ + trackingNumber: order.trackingNumber, + labelUrl, + packingSlipBase64: buildPackingSlipBase64(order), + shippingMethod: order.shippingMethod || '' + }); + } + + // Prevent duplicate shipment creation from concurrent requests + if (order.status === 'SHIPPED') { + throw error(409, 'Order has already been shipped'); + } + + // Calculate package totals from items + const MAX_HEIGHT_IN = 48; // Cap at 48 inches to prevent unrealistic box dimensions + let totalWeight = 0; + let maxLength = 0; + let maxWidth = 0; + let totalHeight = 0; + + for (const oi of order.items) { + const item = oi.warehouseItem; + totalWeight += item.weightGrams * oi.quantity; + maxLength = Math.max(maxLength, item.lengthIn); + maxWidth = Math.max(maxWidth, item.widthIn); + totalHeight += item.heightIn * oi.quantity; + } + totalHeight = Math.min(totalHeight, MAX_HEIGHT_IN); + + const packingSlipBase64 = buildPackingSlipBase64(order); + + let trackingNumber: string | null = null; + let labelUrl: string | null = null; + let shippingMethod: string; + + const isChitChatsOrder = !carrier || carrier === 'auto' + ? order.estimatedServiceCode?.startsWith('CHITCHATS.') + : carrier === 'chitchats'; + + if (isChitChatsOrder) { + // ── CHIT CHATS PATH ── + shippingMethod = 'chitchats'; + + try { + const result = await createChitChatsShipment({ + order, + weightGrams: totalWeight, + lengthIn: maxLength, + widthIn: maxWidth, + heightIn: totalHeight + }); + trackingNumber = result.trackingNumber; + labelUrl = result.labelBase64; + } catch (e: any) { + console.error('Chit Chats Create Shipment error:', e.message); + throw error(502, 'Chit Chats shipment creation failed'); + } + } else if (carrier === 'lettermail' || ((!carrier || carrier === 'auto') && isLettermail(order.estimatedServiceName))) { + // ── LETTERMAIL PATH: Use Theseus/mail.hackclub.com ── + shippingMethod = 'lettermail'; + + const theseusApiKey = env.THESEUS_API_KEY; + const theseusQueueSlug = env.THESEUS_QUEUE_SLUG; + + if (!theseusApiKey || !theseusQueueSlug) { + throw error(500, 'Theseus API not configured (THESEUS_API_KEY and THESEUS_QUEUE_SLUG required)'); + } + + const theseusBody = { + address: { + first_name: order.firstName, + last_name: order.lastName, + line_1: order.addressLine1, + line_2: order.addressLine2 || undefined, + city: order.city, + state: order.stateProvince, + postal_code: order.postalCode || undefined, + country: order.country + }, + idempotency_key: `warehouse-order-${order.id}`, + metadata: { + warehouse_order_id: order.id, + fulfillment_id: order.fulfillmentId + } + }; + + const theseusUrl = `${env.THESEUS_BASE_URL || 'https://mail.hackclub.com'}/api/v1/letter_queues/instant/${theseusQueueSlug}`; + + const theseusRes = await fetch(theseusUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${theseusApiKey}` + }, + body: JSON.stringify(theseusBody) + }); + + if (!theseusRes.ok) { + const errBody = await theseusRes.text(); + console.error('Theseus API error:', errBody); + throw error(502, `Lettermail label creation failed: ${theseusRes.status}`); + } + + const theseusData = await theseusRes.json(); + trackingNumber = theseusData.id || null; + const rawLabelUrl = theseusData.label_url || null; + + // Fetch the label PDF and convert to base64 data URL so the frontend can print it via qz-tray + if (rawLabelUrl) { + try { + const labelRes = await fetch(rawLabelUrl); + if (labelRes.ok) { + const labelBuffer = await labelRes.arrayBuffer(); + const labelBase64 = arrayBufferToBase64(labelBuffer); + labelUrl = `data:application/pdf;base64,${labelBase64}`; + } else { + labelUrl = rawLabelUrl; + } + } catch { + labelUrl = rawLabelUrl; + } + } + + // Mark the letter as printed in Theseus + if (trackingNumber) { + const theseusBaseUrl = env.THESEUS_BASE_URL || 'https://mail.hackclub.com'; + await fetch(`${theseusBaseUrl}/api/v1/letters/${trackingNumber}/mark_printed`, { + method: 'POST', + headers: { Authorization: `Bearer ${theseusApiKey}` } + }).catch((e) => console.error('Failed to mark Theseus letter as printed:', e)); + } + + } else { + // ── CANADA POST PARCEL PATH ── + shippingMethod = 'canada_post'; + + const weightKg = totalWeight * GRAMS_TO_KG; + const lengthCm = inchesToCm(maxLength); + const widthCm = inchesToCm(maxWidth); + const heightCm = inchesToCm(totalHeight); + let serviceCode = order.estimatedServiceName + ? getServiceCode(order.estimatedServiceName) + : order.country === 'CA' ? 'DOM.RP' + : order.country === 'US' ? 'USA.TP' + : 'INT.TP'; + if (!env.CP_CONTRACT_ID) { + if (serviceCode === 'USA.SP.AIR') serviceCode = 'USA.TP'; + } + console.log(`Creating shipment: country=${order.country}, estimatedService=${order.estimatedServiceName}, serviceCode=${serviceCode}`); + + // Try creating shipment, fall back to alternate international services if unavailable + const fallbackCodes: string[] = []; + const intFallbacks = ['INT.TP', 'INT.SP.AIR', 'INT.IP', 'INT.IP.SURF']; + if (env.CP_CONTRACT_ID) intFallbacks.unshift('INT.XP'); + if (serviceCode.startsWith('INT.')) { + for (const code of intFallbacks) { + if (code !== serviceCode) fallbackCodes.push(code); + } + } + if (serviceCode === 'INT.XP') fallbackCodes.push('INT.TP'); + let lastError: any; + for (const code of [serviceCode, ...fallbackCodes]) { + try { + const result = await createShipment({ order, weightKg, lengthCm, widthCm, heightCm, serviceCode: code }); + trackingNumber = result.trackingPin; + labelUrl = result.labelBase64; + if (code !== serviceCode) console.log(`Used fallback service ${code} instead of ${serviceCode}`); + lastError = null; + break; + } catch (e: any) { + console.error(`Canada Post Create Shipment error (${code}):`, e.message); + lastError = e; + } + } + // If Canada Post failed for international, try Chit Chats as fallback + if (lastError && order.country !== 'CA' && env.CHITCHATS_ACCESS_TOKEN && env.CHITCHATS_CLIENT_ID) { + console.log(`Canada Post unavailable for ${order.country}, trying Chit Chats fallback`); + try { + const result = await createChitChatsShipment({ + order, + weightGrams: totalWeight, + lengthIn: maxLength, + widthIn: maxWidth, + heightIn: totalHeight + }); + trackingNumber = result.trackingNumber; + labelUrl = result.labelBase64; + shippingMethod = 'chitchats'; + lastError = null; + } catch (e: any) { + console.error('Chit Chats fallback also failed:', e.message); + } + } + if (lastError) { + throw error(502, `Shipment creation failed: ${lastError.message}`); + } + } + + // Update the order with tracking info + await db.update(warehouseOrder) + .set({ + trackingNumber, + labelUrl, + shippingMethod, + status: 'SHIPPED', + updatedAt: new Date() + }) + .where(eq(warehouseOrder.id, orderId)); + + return json({ + trackingNumber, + labelUrl, + packingSlipBase64, + shippingMethod + }); +}; diff --git a/resolution-frontend/src/routes/api/qz/cert/+server.ts b/resolution-frontend/src/routes/api/qz/cert/+server.ts new file mode 100644 index 0000000..6654852 --- /dev/null +++ b/resolution-frontend/src/routes/api/qz/cert/+server.ts @@ -0,0 +1,14 @@ +import { env } from '$env/dynamic/private'; +import { requireAuth } from '$lib/server/auth/guard'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async (event) => { + requireAuth(event); + const cert = env.QZ_CERTIFICATE?.replace(/\\n/g, '\n'); + if (!cert) { + return new Response('QZ certificate not configured', { status: 500 }); + } + return new Response(cert, { + headers: { 'Content-Type': 'text/plain' } + }); +}; diff --git a/resolution-frontend/src/routes/api/qz/sign/+server.ts b/resolution-frontend/src/routes/api/qz/sign/+server.ts new file mode 100644 index 0000000..f8858c2 --- /dev/null +++ b/resolution-frontend/src/routes/api/qz/sign/+server.ts @@ -0,0 +1,31 @@ +import { env } from '$env/dynamic/private'; +import { createSign } from 'crypto'; +import { requireAuth } from '$lib/server/auth/guard'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async (event) => { + requireAuth(event); + + const toSign = await event.request.text(); + + const privateKey = env.QZ_PRIVATE_KEY?.replace(/\\n/g, '\n'); + const password = env.QZ_PK_PASSWORD; + + if (!privateKey) { + return new Response('QZ private key not configured', { status: 500 }); + } + + try { + const sign = createSign('SHA512'); + sign.update(toSign); + const signature = sign.sign( + { key: privateKey, passphrase: password || '' }, + 'base64' + ); + return new Response(signature, { + headers: { 'Content-Type': 'text/plain' } + }); + } catch (e) { + return new Response('Signing failed', { status: 500 }); + } +}; diff --git a/resolution-frontend/src/routes/api/shipping-rates/+server.ts b/resolution-frontend/src/routes/api/shipping-rates/+server.ts new file mode 100644 index 0000000..f2d897c --- /dev/null +++ b/resolution-frontend/src/routes/api/shipping-rates/+server.ts @@ -0,0 +1,147 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { env } from '$env/dynamic/private'; +import { requireAuth } from '$lib/server/auth/guard'; +import { validateJson, shippingRateSchema } from '$lib/server/validation'; +import { db } from '$lib/server/db'; +import { ambassadorPathway } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { GRAMS_TO_KG, inchesToCm, fetchRates, getLetterMailOptions, calculateZonosDuties } from '$lib/server/canada-post'; +import { getCadToUsdRate } from '$lib/server/exchange-rate'; +import { fetchChitChatsRates } from '$lib/server/chit-chats'; + +export const POST: RequestHandler = async (event) => { + const { user } = requireAuth(event); + + const assignments = await db + .select() + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, user.id)); + + if (assignments.length === 0 && !user.isAdmin) { + throw error(403, 'You are not an ambassador'); + } + + const data = await validateJson(shippingRateSchema, event.request); + + if ((data.country === 'CA' || data.country === 'US') && !data.postalCode) { + throw error(400, 'Postal/ZIP code is required for Canadian and US destinations'); + } + + if (!env.CP_ORIGIN_POSTAL_CODE) { + throw error(500, 'Canada Post API not configured'); + } + + const weightKg = data.weight * GRAMS_TO_KG; + + // For flats/envelopes, snap to available envelope sizes (4x6in or 6x9in). + // If too large for 6x9, treat as a bubble packet with 0.5in thickness. + let effectiveLength = data.length; + let effectiveWidth = data.width; + let effectivePackageType = data.packageType; + if (data.packageType === 'flat' || data.packageType === 'envelope') { + const l = Math.max(data.length, data.width); + const w = Math.min(data.length, data.width); + if (l <= 6 && w <= 4) { + effectiveLength = 6; + effectiveWidth = 4; + } else if (l <= 9 && w <= 6) { + effectiveLength = 9; + effectiveWidth = 6; + } else { + // Too large for available envelopes — treat as bubble packet + effectiveLength = l; + effectiveWidth = w; + effectivePackageType = 'box'; + } + } + + const lengthCm = inchesToCm(effectiveLength); + const widthCm = inchesToCm(effectiveWidth); + const heightCm = effectivePackageType === 'box' + ? inchesToCm(data.packageType === 'box' ? data.height : 0.5) + : 0.5; + + const lettermailOptions = getLetterMailOptions(data.weight, lengthCm, widthCm, heightCm, data.country); + + let parcelRates: Array<{ + serviceName: string; + serviceCode: string; + priceDetails: { base: number; gst: number; pst: number; hst: number; total: number }; + deliveryDate: string; + transitDays: string; + currency: string; + }> = []; + try { + parcelRates = await fetchRates({ country: data.country, postalCode: data.postalCode, weightKg, lengthCm, widthCm, heightCm }); + } catch (err) { + console.error('Parcel rate lookup failed:', err); + } + + let chitChatsRates: Array<{ + serviceName: string; + serviceCode: string; + priceDetails: { base: number; gst: number; pst: number; hst: number; total: number }; + deliveryDate: string; + transitDays: string; + currency: string; + }> = []; + try { + if (env.CHITCHATS_ACCESS_TOKEN && env.CHITCHATS_CLIENT_ID) { + // Chit Chats requires a temporary shipment to return rates — use actual address data + chitChatsRates = await fetchChitChatsRates({ + country: data.country, + postalCode: data.postalCode, + province: data.province, + name: 'Rate Quote', + address1: data.street, + city: data.city, + weightGrams: data.weight, + lengthIn: effectiveLength, + widthIn: effectiveWidth, + heightIn: data.packageType === 'box' ? data.height : 0.5, + valueCad: 1.00 + }); + } + } catch (err) { + console.error('Chit Chats rate lookup failed:', err); + } + + const allRates = [...lettermailOptions, ...parcelRates, ...chitChatsRates]; + + let zonosDuties = null; + if (data.country === 'US' && data.items && data.items.length > 0) { + const cheapestRate = allRates.reduce((min, r) => r.priceDetails.total < min ? r.priceDetails.total : min, Infinity); + const cadToUsd = await getCadToUsdRate(); + const shippingCostCad = cheapestRate !== Infinity ? cheapestRate / cadToUsd : 0; + + zonosDuties = await calculateZonosDuties({ + items: data.items.map((item: any) => ({ + hsCode: item.hsCode || '', + valueCadCents: item.costCents || 0, + quantity: item.quantity || 1, + sku: item.sku || '', + description: item.name || 'Merchandise' + })), + shippingCostCad, + destinationAddress: { + city: data.city || '', + state: data.province || '', + postalCode: data.postalCode || '', + country: data.country + } + }); + } + + return json({ + rates: allRates, + origin: env.CP_ORIGIN_POSTAL_CODE, + destination: { + country: data.country, + city: data.city, + province: data.province, + postalCode: data.postalCode + }, + ...(zonosDuties ? { zonosDuties } : {}) + }); +}; diff --git a/resolution-frontend/src/routes/app/+page.svelte b/resolution-frontend/src/routes/app/+page.svelte index 79b64bb..0f8e186 100644 --- a/resolution-frontend/src/routes/app/+page.svelte +++ b/resolution-frontend/src/routes/app/+page.svelte @@ -46,13 +46,17 @@

Welcome, {data.user.firstName || data.user.email}!

- {#if data.isReviewer || data.user.isAdmin} - Reviewer + {#if data.isAmbassador || data.user.isAdmin} + Warehouse + {/if} + {#if data.isReviewer || data.user.isAdmin} + Reviewer {/if} {#if data.isAmbassador} Ambassador {/if} {#if data.user.isAdmin} + Warehouse Backend Admin {/if}
@@ -191,6 +195,8 @@ .admin-btn, .ambassador-btn, + .warehouse-btn, + .warehouse-backend-btn, .reviewer-btn { padding: 0.5rem 1rem; background: rgba(255, 255, 255, 0.8); @@ -209,6 +215,16 @@ color: #a633d6; } + .warehouse-btn { + border: 1px solid #338eda; + color: #338eda; + } + + .warehouse-backend-btn { + border: 1px solid #ff8c37; + color: #ff8c37; + } + .reviewer-btn { border: 1px solid #ff8c37; color: #ff8c37; @@ -216,6 +232,8 @@ .admin-btn:hover, .ambassador-btn:hover, + .warehouse-btn:hover, + .warehouse-backend-btn:hover, .reviewer-btn:hover { background: rgba(255, 255, 255, 1); } diff --git a/resolution-frontend/src/routes/app/admin/+page.svelte b/resolution-frontend/src/routes/app/admin/+page.svelte index b0fd841..03d45f4 100644 --- a/resolution-frontend/src/routes/app/admin/+page.svelte +++ b/resolution-frontend/src/routes/app/admin/+page.svelte @@ -1,6 +1,5 @@ + + + Warehouse Backend - Resolution + + +
+ + Back + Back to Dashboard + + +
+

Warehouse Backend

+

Admin inventory management

+
+ + + + {@render children()} +
+ + diff --git a/resolution-frontend/src/routes/app/warehouse-backend/+page.server.ts b/resolution-frontend/src/routes/app/warehouse-backend/+page.server.ts new file mode 100644 index 0000000..2eee5c8 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse-backend/+page.server.ts @@ -0,0 +1,235 @@ +import type { PageServerLoad, Actions } from './$types'; +import { db } from '$lib/server/db'; +import { warehouseItem, warehouseCategory } from '$lib/server/db/schema'; +import { eq, asc, desc } from 'drizzle-orm'; +import { error, fail } from '@sveltejs/kit'; + +function requireAdmin(locals: App.Locals) { + if (!locals.user || !locals.session) { + throw error(401, 'Unauthorized'); + } + if (!locals.user.isAdmin) { + throw error(403, 'Access denied - admin only'); + } +} + +export const load: PageServerLoad = async () => { + const [categories, items] = await Promise.all([ + db.select().from(warehouseCategory).orderBy(asc(warehouseCategory.sortOrder)), + db.select().from(warehouseItem).orderBy(desc(warehouseItem.createdAt)) + ]); + + return { categories, items }; +}; + +export const actions: Actions = { + createCategory: async ({ request, locals }) => { + requireAdmin(locals); + const formData = await request.formData(); + const name = formData.get('name') as string; + const sortOrder = parseInt(formData.get('sortOrder') as string); + + if (!name) { + return fail(400, { error: 'Category name is required' }); + } + + await db.insert(warehouseCategory).values({ + name, + sortOrder: isNaN(sortOrder) ? 0 : sortOrder + }); + + return { success: true }; + }, + + updateCategory: async ({ request, locals }) => { + requireAdmin(locals); + const formData = await request.formData(); + const id = formData.get('id') as string; + const name = formData.get('name') as string; + const sortOrder = parseInt(formData.get('sortOrder') as string); + + if (!id) { + return fail(400, { error: 'Category ID is required' }); + } + if (!name) { + return fail(400, { error: 'Category name is required' }); + } + + const existing = await db.select({ id: warehouseCategory.id }).from(warehouseCategory).where(eq(warehouseCategory.id, id)).limit(1); + if (existing.length === 0) { + return fail(404, { error: 'Category not found' }); + } + + await db + .update(warehouseCategory) + .set({ + name, + sortOrder: isNaN(sortOrder) ? 0 : sortOrder + }) + .where(eq(warehouseCategory.id, id)); + + return { success: true }; + }, + + deleteCategory: async ({ request, locals }) => { + requireAdmin(locals); + const formData = await request.formData(); + const id = formData.get('id') as string; + + if (!id) { + return fail(400, { error: 'Category ID is required' }); + } + + const existing = await db.select({ id: warehouseCategory.id }).from(warehouseCategory).where(eq(warehouseCategory.id, id)).limit(1); + if (existing.length === 0) { + return fail(404, { error: 'Category not found' }); + } + + await db.delete(warehouseCategory).where(eq(warehouseCategory.id, id)); + + return { success: true }; + }, + + createItem: async ({ request, locals }) => { + requireAdmin(locals); + const formData = await request.formData(); + const name = formData.get('name') as string; + const sku = formData.get('sku') as string; + const categoryId = formData.get('categoryId') as string | null; + const sizing = formData.get('sizing') as string | null; + const packageType = formData.get('packageType') as string; + const lengthIn = parseFloat(formData.get('lengthIn') as string); + const widthIn = parseFloat(formData.get('widthIn') as string); + const heightIn = parseFloat(formData.get('heightIn') as string); + const weightGrams = parseFloat(formData.get('weightGrams') as string); + const costDollars = parseFloat(formData.get('costDollars') as string); + const hsCode = formData.get('hsCode') as string; + const quantity = parseInt(formData.get('quantity') as string); + const imageUrl = formData.get('imageUrl') as string | null; + + if (!name) { + return fail(400, { error: 'Item name is required' }); + } + if (!sku) { + return fail(400, { error: 'SKU is required' }); + } + if (isNaN(lengthIn) || isNaN(widthIn) || isNaN(heightIn)) { + return fail(400, { error: 'Valid dimensions are required' }); + } + if (isNaN(weightGrams)) { + return fail(400, { error: 'Valid weight is required' }); + } + if (isNaN(costDollars)) { + return fail(400, { error: 'Valid cost is required' }); + } + if (!hsCode) { + return fail(400, { error: 'HS Code is required' }); + } + + await db.insert(warehouseItem).values({ + name, + sku, + categoryId: categoryId || null, + sizing: sizing || null, + packageType: packageType || 'box', + lengthIn, + widthIn, + heightIn, + weightGrams, + costCents: Math.round(costDollars * 100), + hsCode, + quantity: isNaN(quantity) ? 0 : quantity, + imageUrl: imageUrl || null + }); + + return { success: true }; + }, + + updateItem: async ({ request, locals }) => { + requireAdmin(locals); + const formData = await request.formData(); + const id = formData.get('id') as string; + const name = formData.get('name') as string; + const sku = formData.get('sku') as string; + const categoryId = formData.get('categoryId') as string | null; + const sizing = formData.get('sizing') as string | null; + const packageType = formData.get('packageType') as string; + const lengthIn = parseFloat(formData.get('lengthIn') as string); + const widthIn = parseFloat(formData.get('widthIn') as string); + const heightIn = parseFloat(formData.get('heightIn') as string); + const weightGrams = parseFloat(formData.get('weightGrams') as string); + const costDollars = parseFloat(formData.get('costDollars') as string); + const hsCode = formData.get('hsCode') as string; + const quantity = parseInt(formData.get('quantity') as string); + const imageUrl = formData.get('imageUrl') as string | null; + + if (!id) { + return fail(400, { error: 'Item ID is required' }); + } + + const existing = await db.select({ id: warehouseItem.id }).from(warehouseItem).where(eq(warehouseItem.id, id)).limit(1); + if (existing.length === 0) { + return fail(404, { error: 'Item not found' }); + } + + if (!name) { + return fail(400, { error: 'Item name is required' }); + } + if (!sku) { + return fail(400, { error: 'SKU is required' }); + } + if (isNaN(lengthIn) || isNaN(widthIn) || isNaN(heightIn)) { + return fail(400, { error: 'Valid dimensions are required' }); + } + if (isNaN(weightGrams)) { + return fail(400, { error: 'Valid weight is required' }); + } + if (isNaN(costDollars)) { + return fail(400, { error: 'Valid cost is required' }); + } + if (!hsCode) { + return fail(400, { error: 'HS Code is required' }); + } + + await db + .update(warehouseItem) + .set({ + name, + sku, + categoryId: categoryId || null, + sizing: sizing || null, + packageType: packageType || 'box', + lengthIn, + widthIn, + heightIn, + weightGrams, + costCents: Math.round(costDollars * 100), + hsCode, + quantity: isNaN(quantity) ? 0 : quantity, + imageUrl: imageUrl || null, + updatedAt: new Date() + }) + .where(eq(warehouseItem.id, id)); + + return { success: true }; + }, + + deleteItem: async ({ request, locals }) => { + requireAdmin(locals); + const formData = await request.formData(); + const id = formData.get('id') as string; + + if (!id) { + return fail(400, { error: 'Item ID is required' }); + } + + const existing = await db.select({ id: warehouseItem.id }).from(warehouseItem).where(eq(warehouseItem.id, id)).limit(1); + if (existing.length === 0) { + return fail(404, { error: 'Item not found' }); + } + + await db.delete(warehouseItem).where(eq(warehouseItem.id, id)); + + return { success: true }; + } +}; diff --git a/resolution-frontend/src/routes/app/warehouse-backend/+page.svelte b/resolution-frontend/src/routes/app/warehouse-backend/+page.svelte new file mode 100644 index 0000000..e734565 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse-backend/+page.svelte @@ -0,0 +1,638 @@ + + + +
+

Categories

+ + {#if data.categories.length === 0} +

No categories yet.

+ {:else} + + + + + + + + + + {#each data.categories as cat (cat.id)} + {#if editingCategoryId === cat.id} + + + + {:else} + + + + + + {/if} + {/each} + +
NameSort OrderActions
+ { return async ({ update }) => { await update(); cancelEditCategory(); }; }}> + +
+ + + + +
+ +
{cat.name}{cat.sortOrder} + +
{ if (!confirm('Are you sure you want to delete this category?')) e.preventDefault(); }} use:enhance class="inline-form"> + + +
+
+ {/if} + +

Add Category

+
+
+ + + +
+
+
+ + +
+

Items

+ + {#if data.items.length === 0} +

No items in the warehouse yet.

+ {:else} + {#each groupedItems() as group} +
+

{group.category?.name || 'Uncategorized'}

+ {#if group.items.length === 0} +

No items in this category.

+ {:else} +
+ + + + + + + + + + + + + + + + {#each group.items as item (item.id)} + + + + + + + + + + + + {/each} + +
PhotoNameSKUOptionsDimensionsWeightCostQtyActions
+ {#if item.imageUrl} + + {:else} + + {/if} + {item.name}{item.sku}{item.sizing || '—'} + {item.packageType === 'flat' + ? `${item.lengthIn}×${item.widthIn} in (flat)` + : `${item.lengthIn}×${item.widthIn}×${item.heightIn} in`} + {item.weightGrams} g{formatCost(item.costCents)}{item.quantity} + +
{ if (!confirm('Are you sure you want to delete this item?')) e.preventDefault(); }} use:enhance class="inline-form"> + + +
+
+
+ {/if} +
+ {/each} + {/if} +
+ + +{#if editingItemId} + +{/if} + + +
+

Add New Item

+
+
+ + + + + + + + + + + + + +
+ +
+
+ + +{#if expandedImage} + +{/if} + + diff --git a/resolution-frontend/src/routes/app/warehouse-backend/fulfillment/+page.server.ts b/resolution-frontend/src/routes/app/warehouse-backend/fulfillment/+page.server.ts new file mode 100644 index 0000000..e67f84d --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse-backend/fulfillment/+page.server.ts @@ -0,0 +1,52 @@ +import type { PageServerLoad } from './$types'; +import { db } from '$lib/server/db'; +import { warehouseOrder, warehouseOrderTag } from '$lib/server/db/schema'; +import { ne, desc, count } from 'drizzle-orm'; + +const PAGE_SIZE = 50; + +export const load: PageServerLoad = async ({ parent, url }) => { + await parent(); + + const page = Math.max(1, parseInt(url.searchParams.get('page') || '1')); + const offset = (page - 1) * PAGE_SIZE; + + const [orders, [{ total }]] = await Promise.all([ + db.query.warehouseOrder.findMany({ + where: ne(warehouseOrder.status, 'DRAFT'), + with: { + createdBy: { + columns: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + items: { + with: { + warehouseItem: true + } + }, + tags: true + }, + orderBy: [desc(warehouseOrder.createdAt)], + limit: PAGE_SIZE, + offset + }), + db.select({ total: count() }).from(warehouseOrder).where(ne(warehouseOrder.status, 'DRAFT')) + ]); + + const allTags = await db + .selectDistinct({ tag: warehouseOrderTag.tag }) + .from(warehouseOrderTag) + .orderBy(warehouseOrderTag.tag); + + return { + orders, + allTags: allTags.map((t) => t.tag), + page, + pageSize: PAGE_SIZE, + totalOrders: total + }; +}; diff --git a/resolution-frontend/src/routes/app/warehouse-backend/fulfillment/+page.svelte b/resolution-frontend/src/routes/app/warehouse-backend/fulfillment/+page.svelte new file mode 100644 index 0000000..00b1fbb --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse-backend/fulfillment/+page.svelte @@ -0,0 +1,392 @@ + + +
+
+ Status: + + {#each statuses as s} + {@const count = data.orders.filter((o: any) => o.status === s).length} + {#if count > 0} + + {/if} + {/each} +
+ + {#if data.allTags.length > 0} +
+ Tag: + + {#each data.allTags as tag} + + {/each} +
+ {/if} +
+ +{#if filteredOrders().length === 0} +
+

No orders to fulfill.

+

Placed orders will appear here.

+
+{:else} +
+
+ + + + + + + + + + + + + + + + + {#each filteredOrders() as order (order.id)} + + + + + + + + + + + + + {#if expandedOrder === order.id} + + + + {/if} + {/each} + +
IDRecipientOrdered ByDestinationItemsEst. ShippingStatusTagsCreatedActions
#{order.fulfillmentId} + {order.firstName} {order.lastName} +
{order.email} +
+ {formatCreatorName(order.createdBy)} +
{order.createdBy.email} +
+ {order.city}, {order.stateProvince} +
{order.country}{order.postalCode ? ` ${order.postalCode}` : ''} +
+ {order.items.length} item{order.items.length !== 1 ? 's' : ''} +
{order.items.reduce((sum: number, oi: any) => sum + oi.quantity, 0)} total qty +
+ {#if order.estimatedShippingCents} + {formatCost(order.estimatedShippingCents)} +
{order.estimatedServiceName || '—'} + {:else} + Not estimated + {/if} +
{statusLabel(order.status)} +
+ {#each order.tags as tagObj} + {tagObj.tag} + {/each} +
+
{new Date(order.createdAt).toLocaleDateString()} + +
+
+
+

Recipient

+

{order.firstName} {order.lastName}

+

{order.email}{order.phone ? ` · ${order.phone}` : ''}

+
+
+

Address

+

{order.addressLine1}

+ {#if order.addressLine2}

{order.addressLine2}

{/if} +

{order.city}, {order.stateProvince} {order.postalCode || ''}

+

{order.country}

+
+
+

Items

+ {#each order.items as oi} +

+ {oi.warehouseItem.name} + {#if oi.sizingChoice}({oi.sizingChoice}){/if} + × {oi.quantity} +

+ {/each} +
+
+

Estimated Package

+ {#if order.estimatedTotalLengthIn} +

Dimensions: {order.estimatedTotalLengthIn}×{order.estimatedTotalWidthIn}{order.estimatedPackageType !== 'flat' ? `×${order.estimatedTotalHeightIn}` : ''} in ({order.estimatedPackageType})

+

Weight: {order.estimatedTotalWeightGrams}g

+ {/if} + {#if order.estimatedShippingCents} +

Shipping: {formatCost(order.estimatedShippingCents)} ({order.estimatedServiceName})

+ {:else} +

Shipping: Not estimated

+ {/if} +
+ {#if order.notes} +
+

Notes

+

{order.notes}

+
+ {/if} +
+
+
+
+{/if} + + diff --git a/resolution-frontend/src/routes/app/warehouse/+layout.svelte b/resolution-frontend/src/routes/app/warehouse/+layout.svelte new file mode 100644 index 0000000..d795a02 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/+layout.svelte @@ -0,0 +1,108 @@ + + + + Warehouse - Resolution + + +
+ + Back + Back to Dashboard + + +
+

Warehouse

+

Inventory management

+
+ + + + {@render children()} +
+ + diff --git a/resolution-frontend/src/routes/app/warehouse/+page.server.ts b/resolution-frontend/src/routes/app/warehouse/+page.server.ts new file mode 100644 index 0000000..9cda77c --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/+page.server.ts @@ -0,0 +1,6 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + throw redirect(302, '/app/warehouse/items'); +}; diff --git a/resolution-frontend/src/routes/app/warehouse/+page.svelte b/resolution-frontend/src/routes/app/warehouse/+page.svelte new file mode 100644 index 0000000..1ef0183 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/+page.svelte @@ -0,0 +1 @@ +

Redirecting...

diff --git a/resolution-frontend/src/routes/app/warehouse/batches/+page.server.ts b/resolution-frontend/src/routes/app/warehouse/batches/+page.server.ts new file mode 100644 index 0000000..2997179 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/batches/+page.server.ts @@ -0,0 +1,498 @@ +import type { PageServerLoad, Actions } from './$types'; +import { db } from '$lib/server/db'; +import { warehouseBatch, warehouseBatchTag, warehouseOrderTemplate, warehouseOrder, warehouseOrderItem, warehouseOrderTag, warehouseItem, ambassadorPathway } from '$lib/server/db/schema'; +import { eq, and, gte, desc, asc, sql, inArray } from 'drizzle-orm'; +import { error, fail } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import { fetchCheapestRate } from '$lib/server/canada-post'; +import { resolveCountryCode, resolveStateCode } from '$lib/server/countries'; +import Papa from 'papaparse'; + +function parseCsv(raw: string): string[][] { + const result = Papa.parse(raw, { header: false, skipEmptyLines: true }); + return result.data; +} + +export const load: PageServerLoad = async ({ parent }) => { + const { user } = await parent(); + + const ambassadorCheck = await db + .select({ userId: ambassadorPathway.userId }) + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, user.id)) + .limit(1); + + const isAmbassador = ambassadorCheck.length > 0; + + if (!user.isAdmin && !isAmbassador) { + throw error(403, 'Access denied'); + } + + const batches = await db.query.warehouseBatch.findMany({ + where: eq(warehouseBatch.createdById, user.id), + with: { + createdBy: true, + template: { + with: { + items: { + with: { + warehouseItem: true + } + } + } + }, + tags: true + }, + orderBy: [desc(warehouseBatch.createdAt)] + }); + + const templates = await db.query.warehouseOrderTemplate.findMany({ + with: { + items: { + with: { + warehouseItem: true + } + } + }, + orderBy: [asc(warehouseOrderTemplate.name)] + }); + + const allTags = await db + .selectDistinct({ tag: warehouseOrderTag.tag }) + .from(warehouseOrderTag) + .orderBy(asc(warehouseOrderTag.tag)); + + return { + batches, + templates, + allTags: allTags.map((t) => t.tag) + }; +}; + +export const actions: Actions = { + createBatch: async ({ request, locals }) => { + const user = locals.user; + if (!user) return fail(401, { error: 'Not logged in' }); + + const formData = await request.formData(); + const title = formData.get('title') as string; + const templateId = formData.get('templateId') as string; + const tagsString = formData.get('tags') as string; + const csvData = formData.get('csvData') as string; + + if (!templateId || !csvData) { + return fail(400, { error: 'Template and CSV data are required' }); + } + + if (csvData.length > 5 * 1024 * 1024) { + return fail(400, { error: 'CSV data exceeds 5MB limit' }); + } + + const rows = parseCsv(csvData); + if (rows.length < 2) { + return fail(400, { error: 'CSV must have a header row and at least one data row' }); + } + const addressCount = rows.length - 1; + + const [batch] = await db.insert(warehouseBatch).values({ + createdById: user.id, + templateId, + title: title || null, + csvData, + addressCount + }).returning({ id: warehouseBatch.id }); + + if (tagsString && tagsString.trim()) { + const tags = tagsString.split(',').map((t) => t.trim()).filter((t) => t.length > 0); + if (tags.length > 0) { + await db.insert(warehouseBatchTag).values( + tags.map((tag) => ({ batchId: batch.id, tag })) + ); + } + } + + return { success: true, batchId: batch.id }; + }, + + mapFields: async ({ request, locals }) => { + const user = locals.user; + if (!user) return fail(401, { error: 'Not logged in' }); + + const formData = await request.formData(); + const batchId = formData.get('batchId') as string; + const fieldMapping = formData.get('fieldMapping') as string; + + if (!batchId || !fieldMapping) { + return fail(400, { error: 'Batch ID and field mapping are required' }); + } + + const batch = await db.query.warehouseBatch.findFirst({ + where: eq(warehouseBatch.id, batchId) + }); + if (!batch) return fail(404, { error: 'Batch not found' }); + if (!user.isAdmin && batch.createdById !== user.id) { + return fail(403, { error: 'Access denied - you do not own this batch' }); + } + + await db.update(warehouseBatch) + .set({ fieldMapping, status: 'MAPPED', updatedAt: new Date() }) + .where(eq(warehouseBatch.id, batchId)); + + return { success: true }; + }, + + processBatch: async ({ request, locals }) => { + const user = locals.user; + if (!user) return fail(401, { error: 'Not logged in' }); + + const formData = await request.formData(); + const batchId = formData.get('batchId') as string; + + if (!batchId) return fail(400, { error: 'Batch ID is required' }); + + const batch = await db.query.warehouseBatch.findFirst({ + where: eq(warehouseBatch.id, batchId), + with: { + template: { + with: { + items: true + } + }, + tags: true + } + }); + + if (!batch) return fail(404, { error: 'Batch not found' }); + if (!user.isAdmin && batch.createdById !== user.id) { + return fail(403, { error: 'Access denied - you do not own this batch' }); + } + if (batch.status !== 'MAPPED') return fail(400, { error: 'Batch must be mapped before processing' }); + if (!batch.fieldMapping) return fail(400, { error: 'No field mapping found' }); + + let mapping: Record; + try { + mapping = JSON.parse(batch.fieldMapping); + } catch { + return fail(400, { error: 'Invalid field mapping data' }); + } + const rows = parseCsv(batch.csvData); + + if (rows.length < 2) return fail(400, { error: 'CSV has no data rows' }); + + const headers = rows[0]; + const dataRows = rows.slice(1); + + // Count valid orders to check total inventory needed + const validOrderCount = dataRows.filter((row) => { + const getValue = (field: string): string => { + const colName = mapping[field]; + if (!colName) return ''; + const colIndex = headers.indexOf(colName); + if (colIndex === -1) return ''; + return (row[colIndex] || '').trim(); + }; + return getValue('firstName') && getValue('lastName') && getValue('email') && + getValue('addressLine1') && getValue('city') && getValue('stateProvince') && getValue('country'); + }).length; + + if (batch.template.items.length > 0) { + const templateItemIds = batch.template.items.map((ti) => ti.warehouseItemId); + const currentStock = await db + .select({ id: warehouseItem.id, name: warehouseItem.name, sku: warehouseItem.sku, quantity: warehouseItem.quantity }) + .from(warehouseItem) + .where(inArray(warehouseItem.id, templateItemIds)); + + const stockMap = new Map(currentStock.map((s) => [s.id, s])); + for (const ti of batch.template.items) { + const needed = ti.quantity * validOrderCount; + const stock = stockMap.get(ti.warehouseItemId); + const available = stock?.quantity ?? 0; + if (needed > available) { + const name = stock?.name ?? ti.warehouseItemId; + return fail(400, { error: `Insufficient stock for "${name}": ${available} available, ${needed} needed for ${validOrderCount} orders` }); + } + } + } + + // Use a transaction so all orders + stock decrements are atomic + try { + await db.transaction(async (tx) => { + for (const row of dataRows) { + const getValue = (field: string): string => { + const colName = mapping[field]; + if (!colName) return ''; + const colIndex = headers.indexOf(colName); + if (colIndex === -1) return ''; + return (row[colIndex] || '').trim(); + }; + + const firstName = getValue('firstName'); + const lastName = getValue('lastName'); + const email = getValue('email'); + const addressLine1 = getValue('addressLine1'); + const city = getValue('city'); + const stateProvince = getValue('stateProvince'); + const rawCountry = getValue('country'); + + if (!firstName || !lastName || !email || !addressLine1 || !city || !stateProvince || !rawCountry) { + continue; + } + + const country = resolveCountryCode(rawCountry); + const resolvedState = resolveStateCode(stateProvince, country); + + const [order] = await tx.insert(warehouseOrder).values({ + createdById: user.id, + batchId, + status: 'APPROVED', + firstName, + lastName, + email, + phone: getValue('phone') || null, + addressLine1, + addressLine2: getValue('addressLine2') || null, + city, + stateProvince: resolvedState, + postalCode: getValue('postalCode') || null, + country + }).returning({ id: warehouseOrder.id }); + + if (batch.template.items.length > 0) { + await tx.insert(warehouseOrderItem).values( + batch.template.items.map((ti) => ({ + orderId: order.id, + warehouseItemId: ti.warehouseItemId, + quantity: ti.quantity + })) + ); + + for (const ti of batch.template.items) { + const result = await tx.update(warehouseItem) + .set({ quantity: sql`${warehouseItem.quantity} - ${ti.quantity}` }) + .where(and(eq(warehouseItem.id, ti.warehouseItemId), gte(warehouseItem.quantity, ti.quantity))); + if (result.rowCount === 0) { + throw new Error('Insufficient stock (concurrent update)'); + } + } + } + + if (batch.tags.length > 0) { + await tx.insert(warehouseOrderTag).values( + batch.tags.map((t) => ({ + orderId: order.id, + tag: t.tag + })) + ); + } + } + + await tx.update(warehouseBatch) + .set({ status: 'PROCESSED', updatedAt: new Date() }) + .where(eq(warehouseBatch.id, batchId)); + }); + } catch (e: any) { + return fail(409, { error: e.message || 'Batch processing failed. Please try again.' }); + } + + return { success: true }; + }, + + calculateBatch: async ({ request, locals }) => { + const user = locals.user; + if (!user) return fail(401, { error: 'Not logged in' }); + + const formData = await request.formData(); + const batchId = formData.get('batchId') as string; + if (!batchId) return fail(400, { error: 'Batch ID is required' }); + + const batch = await db.query.warehouseBatch.findFirst({ + where: eq(warehouseBatch.id, batchId), + with: { + template: { + with: { + items: { + with: { + warehouseItem: true + } + } + } + }, + tags: true + } + }); + + if (!batch) return fail(404, { error: 'Batch not found' }); + if (!user.isAdmin && batch.createdById !== user.id) { + return fail(403, { error: 'Access denied - you do not own this batch' }); + } + if (!batch.fieldMapping) return fail(400, { error: 'No field mapping found' }); + + let mapping: Record; + try { + mapping = JSON.parse(batch.fieldMapping); + } catch { + return fail(400, { error: 'Invalid field mapping data' }); + } + const rows = parseCsv(batch.csvData); + if (rows.length < 2) return fail(400, { error: 'CSV has no data rows' }); + + const headers = rows[0]; + const dataRows = rows.slice(1); + + // Compute package totals from template items + const templateItems = batch.template.items; + let totalWeight = 0; + let maxLength = 0; + let maxWidth = 0; + let totalHeight = 0; + let hasBox = false; + + for (const ti of templateItems) { + const item = ti.warehouseItem; + totalWeight += item.weightGrams * ti.quantity; + maxLength = Math.max(maxLength, item.lengthIn); + maxWidth = Math.max(maxWidth, item.widthIn); + totalHeight += item.heightIn * ti.quantity; + if (item.packageType === 'box') hasBox = true; + } + + const packageType = hasBox || totalHeight > 0.5 ? 'box' : 'flat'; + + // Calculate items cost per order + let itemsCostCentsPerOrder = 0; + for (const ti of templateItems) { + itemsCostCentsPerOrder += ti.warehouseItem.costCents * ti.quantity; + } + + // Check inventory + const inventoryIssues: Array<{ itemName: string; sku: string; available: number; needed: number }> = []; + const validOrderCount = dataRows.filter((row) => { + const getValue = (field: string): string => { + const colName = mapping[field]; + if (!colName) return ''; + const colIndex = headers.indexOf(colName); + if (colIndex === -1) return ''; + return (row[colIndex] || '').trim(); + }; + return getValue('firstName') && getValue('lastName') && getValue('email') && + getValue('addressLine1') && getValue('city') && getValue('stateProvince') && getValue('country'); + }).length; + + for (const ti of templateItems) { + const needed = ti.quantity * validOrderCount; + if (needed > ti.warehouseItem.quantity) { + inventoryIssues.push({ + itemName: ti.warehouseItem.name, + sku: ti.warehouseItem.sku, + available: ti.warehouseItem.quantity, + needed + }); + } + } + + // Build list of valid orders to calculate rates for + const validRows: Array<{ recipient: string; country: string; postalCode?: string; province: string; itemsCostUsd: number }> = []; + for (const row of dataRows) { + const getValue = (field: string): string => { + const colName = mapping[field]; + if (!colName) return ''; + const colIndex = headers.indexOf(colName); + if (colIndex === -1) return ''; + return (row[colIndex] || '').trim(); + }; + + const firstName = getValue('firstName'); + const lastName = getValue('lastName'); + const email = getValue('email'); + const addressLine1 = getValue('addressLine1'); + const city = getValue('city'); + const stateProvince = getValue('stateProvince'); + const rawCountry = getValue('country'); + + if (!firstName || !lastName || !email || !addressLine1 || !city || !stateProvince || !rawCountry) { + continue; + } + + validRows.push({ + recipient: `${firstName} ${lastName}`, + country: resolveCountryCode(rawCountry), + postalCode: getValue('postalCode') || undefined, + province: resolveStateCode(stateProvince, resolveCountryCode(rawCountry)), + itemsCostUsd: itemsCostCentsPerOrder / 100 + }); + } + + // Calculate shipping rates in parallel batches of 5 to prevent timeout on large batches + const BATCH_SIZE = 5; + const orderCosts: Array<{ + recipient: string; + country: string; + itemsCostUsd: number; + shippingCostUsd: number | null; + shippingMethod: string | null; + totalUsd: number | null; + }> = []; + + let totalItemsCostCents = 0; + let totalShippingUsd = 0; + let shippingFailures = 0; + + for (let i = 0; i < validRows.length; i += BATCH_SIZE) { + const chunk = validRows.slice(i, i + BATCH_SIZE); + const results = await Promise.all(chunk.map(async (row) => { + totalItemsCostCents += itemsCostCentsPerOrder; + const rate = await fetchCheapestRate({ + country: row.country, + postalCode: row.postalCode, + province: row.province, + weightGrams: totalWeight, + lengthIn: maxLength, + widthIn: maxWidth, + heightIn: totalHeight, + packageType + }); + return { row, rate }; + })); + + for (const { row, rate } of results) { + if (rate) { + totalShippingUsd += rate.shippingCostUsd; + orderCosts.push({ + recipient: row.recipient, + country: row.country, + itemsCostUsd: row.itemsCostUsd, + shippingCostUsd: rate.shippingCostUsd, + shippingMethod: rate.serviceName, + totalUsd: Math.round((row.itemsCostUsd + rate.shippingCostUsd) * 100) / 100 + }); + } else { + shippingFailures++; + orderCosts.push({ + recipient: row.recipient, + country: row.country, + itemsCostUsd: row.itemsCostUsd, + shippingCostUsd: null, + shippingMethod: null, + totalUsd: null + }); + } + } + } + + const totalItemsCostUsd = Math.round(totalItemsCostCents) / 100; + const grandTotalUsd = Math.round((totalItemsCostUsd + totalShippingUsd) * 100) / 100; + + return { + success: true, + calculation: { + orderCount: validOrderCount, + totalItemsCostUsd, + totalShippingUsd: Math.round(totalShippingUsd * 100) / 100, + grandTotalUsd, + shippingFailures, + inventoryIssues, + orders: orderCosts + } + }; + } +}; diff --git a/resolution-frontend/src/routes/app/warehouse/batches/+page.svelte b/resolution-frontend/src/routes/app/warehouse/batches/+page.svelte new file mode 100644 index 0000000..1962240 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/batches/+page.svelte @@ -0,0 +1,976 @@ + + +{#if mode === 'index'} +
+ +
+ + {#if data.batches.length === 0} +
+

No batches yet.

+

Create a batch to bulk-import orders from a CSV file.

+
+ {:else} +
+
+ + + + + + + + + + + + + + + {#each data.batches as batch (batch.id)} + + + + + + + + + + + {/each} + +
IDTitleTemplateAddressesStatusTagsCreatedActions
{batch.id.slice(0, 8)}{batch.title || '—'}{batch.template.name}{batch.addressCount}{statusLabel(batch.status)} +
+ {#each batch.tags as tagObj} + {tagObj.tag} + {/each} +
+
{new Date(batch.createdAt).toLocaleDateString()} + {#if batch.status === 'AWAITING_MAPPING'} + + {:else if batch.status === 'MAPPED'} + + {:else} + Completed + {/if} +
+
+
+ {/if} +{/if} + +{#if mode === 'create'} +
+ +
+
+

Create Batch

+
{ + return async ({ result, update }) => { + await update(); + if (result.type === 'success' && result.data?.batchId) { + openMap(result.data.batchId as string); + } + }; + }}> + + + + +
+ Tags +
+ {#each tagsArray as tag} + + {tag} + + + {/each} +
+ + {#if tagSuggestions().length > 0} +
    + {#each tagSuggestions() as suggestion} +
  • + +
  • + {/each} +
+ {/if} +
+
+ +
+ +
+ CSV File + ↓ Copy CSV template from Google Sheets + Only accessible with Resolution emails + + {#if csvFileName} +

Loaded: {csvFileName}

+ {/if} + +
+ +
+ +
+
+
+{/if} + +{#if mode === 'map' && activeBatch} +
+ +
+
+

Map CSV Fields

+

+ Map each address field to a column from your CSV. CSV has {csvHeaders().length} column{csvHeaders().length !== 1 ? 's' : ''}. +

+
{ + return async ({ result, update }) => { + await update(); + if (result.type === 'success' && activeBatchId) { + openProcess(activeBatchId); + } + }; + }}> + + + +
+ {#each allFields as field} +
+ + {fieldLabels[field]} + {#if requiredFields.includes(field as any)} + * + {/if} + + +
+ {/each} +
+ +
+ +
+
+
+{/if} + +{#if mode === 'process' && activeBatch} +
+ +
+
+

Process Batch

+ +
+

Batch

+

{activeBatch.title || 'Untitled batch'}

+

{activeBatch.addressCount} address{activeBatch.addressCount !== 1 ? 'es' : ''}

+
+ +
+

Template

+

{activeBatch.template.name}

+ {#each activeBatch.template.items as ti} +

{ti.warehouseItem.name} × {ti.quantity}

+ {/each} +
+ + {#if activeBatch.tags.length > 0} +
+

Tags

+
+ {#each activeBatch.tags as tagObj} + {tagObj.tag} + {/each} +
+
+ {/if} + + {#if calcLoading} +
+

Calculating costs…

+
+ {:else if calcError} +
+
{calcError}
+ +
+ {:else if calculation} + {#if calculation.inventoryIssues.length > 0} +
+

⚠️ Inventory Issues

+ {#each calculation.inventoryIssues as issue} +
+ {issue.itemName} ({issue.sku}): need {issue.needed}, only {issue.available} in stock +
+ {/each} +
+ {/if} + +
+

Cost Summary

+

⚠️ Shipping costs are estimates and may be adjusted. Final costs will be determined at time of shipment.

+
+
+ Orders + {calculation.orderCount} +
+
+ Items Cost + ${calculation.totalItemsCostUsd.toFixed(2)} +
+
+ Shipping Cost + ${calculation.totalShippingUsd.toFixed(2)} +
+ {#if calculation.shippingFailures > 0} +
+ Shipping estimate failed + {calculation.shippingFailures} order{calculation.shippingFailures !== 1 ? 's' : ''} +
+ {/if} +
+ Total + ${calculation.grandTotalUsd.toFixed(2)} +
+
+
+ +
+ Per-Order Breakdown ({calculation.orders.length}) +
+ + + + + + + + + + + + + {#each calculation.orders as order} + + + + + + + + + {/each} + +
RecipientCountryItemsShippingMethodTotal
{order.recipient}{order.country}${order.itemsCostUsd.toFixed(2)}{order.shippingCostUsd != null ? `$${order.shippingCostUsd.toFixed(2)}` : '—'}{order.shippingMethod || '—'}{order.totalUsd != null ? `$${order.totalUsd.toFixed(2)}` : '—'}
+
+
+ {/if} + +
{ + processingBatch = true; + return async ({ update }) => { + processingBatch = false; + await update(); + backToIndex(); + }; + }}> + +
+ +
+
+
+{/if} + + diff --git a/resolution-frontend/src/routes/app/warehouse/fulfillment/+page.server.ts b/resolution-frontend/src/routes/app/warehouse/fulfillment/+page.server.ts new file mode 100644 index 0000000..c833e04 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/fulfillment/+page.server.ts @@ -0,0 +1,57 @@ +import type { PageServerLoad } from './$types'; +import { db } from '$lib/server/db'; +import { warehouseOrder, warehouseOrderTag } from '$lib/server/db/schema'; +import { ne, desc, count } from 'drizzle-orm'; +import { error } from '@sveltejs/kit'; + +const PAGE_SIZE = 50; + +export const load: PageServerLoad = async ({ parent, url }) => { + const { user } = await parent(); + + if (!user.isAdmin) { + throw error(403, 'Access denied - admin only'); + } + + const page = Math.max(1, parseInt(url.searchParams.get('page') || '1')); + const offset = (page - 1) * PAGE_SIZE; + + const [orders, [{ total }]] = await Promise.all([ + db.query.warehouseOrder.findMany({ + where: ne(warehouseOrder.status, 'DRAFT'), + with: { + createdBy: { + columns: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + items: { + with: { + warehouseItem: true + } + }, + tags: true + }, + orderBy: [desc(warehouseOrder.createdAt)], + limit: PAGE_SIZE, + offset + }), + db.select({ total: count() }).from(warehouseOrder).where(ne(warehouseOrder.status, 'DRAFT')) + ]); + + const allTags = await db + .selectDistinct({ tag: warehouseOrderTag.tag }) + .from(warehouseOrderTag) + .orderBy(warehouseOrderTag.tag); + + return { + orders, + allTags: allTags.map((t) => t.tag), + page, + pageSize: PAGE_SIZE, + totalOrders: total + }; +}; diff --git a/resolution-frontend/src/routes/app/warehouse/fulfillment/+page.svelte b/resolution-frontend/src/routes/app/warehouse/fulfillment/+page.svelte new file mode 100644 index 0000000..f8d4c92 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/fulfillment/+page.svelte @@ -0,0 +1,693 @@ + + + + + + +
+ {#if qzStatus === 'connecting'} + ⏳ QZ Tray connecting... + {:else if qzStatus === 'connected'} + 🖨️ Printer ready + {:else} + ✗ QZ Tray not connected — Settings + {/if} +
+ +
+
+ Status: + + {#each statuses as s} + {@const count = data.orders.filter((o: any) => o.status === s).length} + {#if count > 0} + + {/if} + {/each} +
+ + {#if data.allTags.length > 0} +
+ Tag: + + {#each data.allTags as tag} + + {/each} +
+ {/if} +
+ +{#if filteredOrders().length === 0} +
+

No orders to fulfill.

+

Placed orders will appear here.

+
+{:else} +
+
+ + + + + + + + + + + + + + + + + {#each filteredOrders() as order (order.id)} + + + + + + + + + + + + + {#if labelErrors[order.id]} + + + + {/if} + {#if labelResults[order.id]} + + + + {/if} + {#if expandedOrder === order.id} + + + + {/if} + {/each} + +
IDRecipientOrdered ByDestinationItemsEst. ShippingStatusTagsCreatedActions
#{order.fulfillmentId} + {order.firstName} {order.lastName} +
{order.email} +
+ {formatCreatorName(order.createdBy)} +
{order.createdBy.email} +
+ {order.city}, {order.stateProvince} +
{order.country}{order.postalCode ? ` ${order.postalCode}` : ''} +
+ {order.items.length} item{order.items.length !== 1 ? 's' : ''} +
{order.items.reduce((sum: number, oi: any) => sum + oi.quantity, 0)} total qty +
+ {#if order.estimatedShippingCents} + {formatCost(order.estimatedShippingCents)} + {#if order.country === 'US' && order.estimatedDutiesCents} + + {formatCost(order.estimatedDutiesCents)} duties + {/if} +
{order.estimatedServiceName || '—'} + {:else} + Not estimated + {/if} +
{statusLabel(order.status)} +
+ {#each order.tags as tagObj} + {tagObj.tag} + {/each} +
+
{new Date(order.createdAt).toLocaleDateString()} + + {#if order.status === 'APPROVED' && !labelResults[order.id]} + + {/if} + {#if order.status === 'SHIPPED' && !labelResults[order.id]} + + {/if} +
+

✗ {labelErrors[order.id]}

+
+
+
+ {labelResults[order.id].shippingMethod === 'lettermail' ? '✉️ Lettermail' : labelResults[order.id].shippingMethod === 'chitchats' ? '🚀 Chit Chats' : '📦 Canada Post'} + {#if labelResults[order.id].trackingNumber} + Tracking: {labelResults[order.id].trackingNumber} + {/if} + {#if order.country === 'US'} + 🇺🇸 Zonos DDP (duties billed separately) + {/if} +
+
+ + {#if labelResults[order.id].labelUrl} + 📥 Label + {/if} + {#if labelResults[order.id].packingSlipBase64} + 📥 Slip + {/if} +
+
+
+
+
+

Recipient

+

{order.firstName} {order.lastName}

+

{order.email}{order.phone ? ` · ${order.phone}` : ''}

+
+
+

Address

+

{order.addressLine1}

+ {#if order.addressLine2}

{order.addressLine2}

{/if} +

{order.city}, {order.stateProvince} {order.postalCode || ''}

+

{order.country}

+
+
+

Items

+ {#each order.items as oi} +

+ {oi.warehouseItem.name} + {#if oi.sizingChoice}({oi.sizingChoice}){/if} + × {oi.quantity} +

+ {/each} +
+
+

Shipping

+ {#if order.trackingNumber} +

Tracking: {order.trackingNumber}

+ {/if} + {#if order.estimatedShippingCents} +

Cost: {formatCost(order.estimatedShippingCents)} ({order.estimatedServiceName})

+ {:else} +

Shipping: Not estimated

+ {/if} + {#if order.estimatedTotalLengthIn} +

Dimensions: {order.estimatedTotalLengthIn}×{order.estimatedTotalWidthIn}{order.estimatedPackageType !== 'flat' ? `×${order.estimatedTotalHeightIn}` : ''} in ({order.estimatedPackageType})

+

Weight: {order.estimatedTotalWeightGrams}g

+ {/if} +
+ {#if order.notes} +
+

Notes

+

{order.notes}

+
+ {/if} +
+
+
+
+{/if} + + diff --git a/resolution-frontend/src/routes/app/warehouse/items/+page.server.ts b/resolution-frontend/src/routes/app/warehouse/items/+page.server.ts new file mode 100644 index 0000000..44b19a9 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/items/+page.server.ts @@ -0,0 +1,36 @@ +import type { PageServerLoad } from './$types'; +import { db } from '$lib/server/db'; +import { warehouseItem, warehouseCategory, ambassadorPathway } from '$lib/server/db/schema'; +import { eq, desc, asc } from 'drizzle-orm'; +import { error } from '@sveltejs/kit'; + +export const load: PageServerLoad = async ({ parent }) => { + const { user } = await parent(); + + const ambassadorCheck = await db + .select({ userId: ambassadorPathway.userId }) + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, user.id)) + .limit(1); + + const isAmbassador = ambassadorCheck.length > 0; + + if (!user.isAdmin && !isAmbassador) { + throw error(403, 'Access denied'); + } + + const items = await db + .select() + .from(warehouseItem) + .orderBy(desc(warehouseItem.createdAt)); + + const categories = await db + .select() + .from(warehouseCategory) + .orderBy(asc(warehouseCategory.sortOrder), asc(warehouseCategory.name)); + + return { + items, + categories + }; +}; diff --git a/resolution-frontend/src/routes/app/warehouse/items/+page.svelte b/resolution-frontend/src/routes/app/warehouse/items/+page.svelte new file mode 100644 index 0000000..c96cc9f --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/items/+page.svelte @@ -0,0 +1,216 @@ + + +{#if data.items.length === 0} +
+

No items in the warehouse yet.

+

Items will appear here once they are added.

+
+{:else} + {#each groupedItems() as group} +
+

{group.category?.name || 'Uncategorized'}

+ {#if group.items.length === 0} +

No items in this category.

+ {:else} +
+ + + + + + + + + + + + + + + {#each group.items as item (item.id)} + + + + + + + + + + + {/each} + +
PhotoNameIDOptionsDimensionsWeightCostQty
+ {#if item.imageUrl} + + {:else} + + {/if} + {item.name}{item.sku}{item.sizing || '—'}{item.packageType === 'flat' ? `${item.lengthIn}×${item.widthIn} in (flat)` : `${item.lengthIn}×${item.widthIn}×${item.heightIn} in`}{item.weightGrams} g{formatCost(item.costCents)}{item.quantity}
+
+ {/if} +
+ {/each} +{/if} + +{#if expandedImage} + +{/if} + + diff --git a/resolution-frontend/src/routes/app/warehouse/order-templates/+page.server.ts b/resolution-frontend/src/routes/app/warehouse/order-templates/+page.server.ts new file mode 100644 index 0000000..49ec8f6 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/order-templates/+page.server.ts @@ -0,0 +1,158 @@ +import type { PageServerLoad, Actions } from './$types'; +import { db } from '$lib/server/db'; +import { + warehouseItem, + warehouseOrderTemplate, + warehouseOrderTemplateItem, + ambassadorPathway +} from '$lib/server/db/schema'; +import { eq, asc, desc } from 'drizzle-orm'; +import { error, fail } from '@sveltejs/kit'; + +export const load: PageServerLoad = async ({ parent }) => { + const { user } = await parent(); + + const ambassadorCheck = await db + .select({ userId: ambassadorPathway.userId }) + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, user.id)) + .limit(1); + + const isAmbassador = ambassadorCheck.length > 0; + + if (!user.isAdmin && !isAmbassador) { + throw error(403, 'Access denied'); + } + + const [templates, items] = await Promise.all([ + db.query.warehouseOrderTemplate.findMany({ + with: { + createdBy: { + columns: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + items: { + with: { + warehouseItem: true + } + } + }, + orderBy: [desc(warehouseOrderTemplate.createdAt)] + }), + db.select().from(warehouseItem).orderBy(asc(warehouseItem.name)) + ]); + + return { templates, items, userId: user.id, isAdmin: user.isAdmin }; +}; + +export const actions: Actions = { + createTemplate: async ({ request, locals }) => { + const user = locals.user; + if (!user) { + return fail(401, { error: 'Not logged in' }); + } + + if (!user.isAdmin) { + const ambassadorCheck = await db + .select({ userId: ambassadorPathway.userId }) + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, user.id)) + .limit(1); + if (ambassadorCheck.length === 0) { + return fail(403, { error: 'Access denied - admin or ambassador only' }); + } + } + + const formData = await request.formData(); + const name = formData.get('name') as string; + const isPublic = formData.get('isPublic') === 'true'; + const itemsJson = formData.get('items') as string; + + let items: { warehouseItemId: string; quantity: number }[] = []; + try { + items = JSON.parse(itemsJson || '[]'); + } catch { + return fail(400, { error: 'Invalid items data' }); + } + + if (!name || !name.trim()) { + return fail(400, { error: 'Template name is required' }); + } + + if (items.length === 0) { + return fail(400, { error: 'At least one item is required' }); + } + + await db.transaction(async (tx) => { + const [template] = await tx + .insert(warehouseOrderTemplate) + .values({ + createdById: user.id, + name: name.trim(), + isPublic + }) + .returning({ id: warehouseOrderTemplate.id }); + + await Promise.all( + items.map((item) => + tx.insert(warehouseOrderTemplateItem).values({ + templateId: template.id, + warehouseItemId: item.warehouseItemId, + quantity: item.quantity + }) + ) + ); + }); + + return { success: true }; + }, + + deleteTemplate: async ({ request, locals }) => { + const user = locals.user; + if (!user) { + return fail(401, { error: 'Not logged in' }); + } + + if (!user.isAdmin) { + const ambassadorCheck = await db + .select({ userId: ambassadorPathway.userId }) + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, user.id)) + .limit(1); + if (ambassadorCheck.length === 0) { + return fail(403, { error: 'Access denied - admin or ambassador only' }); + } + } + + const formData = await request.formData(); + const templateId = formData.get('templateId') as string; + + if (!templateId) { + return fail(400, { error: 'Template ID is required' }); + } + + const [template] = await db + .select() + .from(warehouseOrderTemplate) + .where(eq(warehouseOrderTemplate.id, templateId)) + .limit(1); + + if (!template) { + return fail(404, { error: 'Template not found' }); + } + + if (template.createdById !== user.id && !user.isAdmin) { + return fail(403, { error: 'Not authorized to delete this template' }); + } + + await db + .delete(warehouseOrderTemplate) + .where(eq(warehouseOrderTemplate.id, templateId)); + + return { success: true }; + } +}; diff --git a/resolution-frontend/src/routes/app/warehouse/order-templates/+page.svelte b/resolution-frontend/src/routes/app/warehouse/order-templates/+page.svelte new file mode 100644 index 0000000..7bf4391 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/order-templates/+page.svelte @@ -0,0 +1,588 @@ + + +
+ +
+ +{#if showCreateForm} +
+

New Template

+
{ + return async ({ result, update }) => { + if (result.type === 'success') { + resetForm(); + } + await update(); + }; + }} + > + + + + +

Items

+
+ + {#if searchResults().length > 0} +
    + {#each searchResults() as item (item.id)} +
  • + +
  • + {/each} +
+ {:else if searchQuery.trim().length > 0} +
No items found
+ {/if} +
+ + {#if addedItems().length > 0} +
+ + + + + + + + + + + {#each addedItems() as item (item.id)} + {@const qty = itemQuantities[item.id] || 0} + + + + + + + {/each} + +
NameSKUQuantity
{item.name}{item.sku} + { itemQuantities[item.id] = parseInt((e.target as HTMLInputElement).value) || 0; }} + /> + + +
+
+ {:else} +

Search above to add items to your template.

+ {/if} + + + + +
+ +
+
+
+{/if} + +
+

Your Templates

+ {#if userTemplates.length === 0} +

You haven't created any templates yet.

+ {:else} +
+ {#each userTemplates as template (template.id)} +
+
+

{template.name}

+ {#if template.isPublic} + Public + {/if} +
+
    + {#each template.items as ti} +
  • + {ti.warehouseItem.name} + × {ti.quantity} +
  • + {/each} +
+ +
+ {/each} +
+ {/if} +
+ +
+

Public Templates

+ {#if publicTemplates.length === 0} +

No public templates from other users yet.

+ {:else} +
+ {#each publicTemplates as template (template.id)} +
+
+

{template.name}

+ Public +
+

by {template.createdBy.firstName ? `${template.createdBy.firstName} ${template.createdBy.lastName || ''}`.trim() : template.createdBy.email}

+
    + {#each template.items as ti} +
  • + {ti.warehouseItem.name} + × {ti.quantity} +
  • + {/each} +
+ +
+ {/each} +
+ {/if} +
+ + diff --git a/resolution-frontend/src/routes/app/warehouse/orders/+page.server.ts b/resolution-frontend/src/routes/app/warehouse/orders/+page.server.ts new file mode 100644 index 0000000..96bc5bb --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/orders/+page.server.ts @@ -0,0 +1,58 @@ +import type { PageServerLoad } from './$types'; +import { db } from '$lib/server/db'; +import { warehouseItem, warehouseOrder, warehouseOrderTag, ambassadorPathway } from '$lib/server/db/schema'; +import { eq, desc } from 'drizzle-orm'; +import { error } from '@sveltejs/kit'; + +export const load: PageServerLoad = async ({ parent }) => { + const { user } = await parent(); + + const ambassadorCheck = await db + .select({ userId: ambassadorPathway.userId }) + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, user.id)) + .limit(1); + + const isAmbassador = ambassadorCheck.length > 0; + + if (!user.isAdmin && !isAmbassador) { + throw error(403, 'Access denied'); + } + + const items = await db + .select() + .from(warehouseItem) + .orderBy(warehouseItem.name); + + const orders = await db.query.warehouseOrder.findMany({ + where: user.isAdmin ? undefined : eq(warehouseOrder.createdById, user.id), + with: { + createdBy: { + columns: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + items: { + with: { + warehouseItem: true + } + }, + tags: true + }, + orderBy: [desc(warehouseOrder.createdAt)] + }); + + const allTags = await db + .selectDistinct({ tag: warehouseOrderTag.tag }) + .from(warehouseOrderTag) + .orderBy(warehouseOrderTag.tag); + + return { + items, + orders, + allTags: allTags.map((t) => t.tag) + }; +}; diff --git a/resolution-frontend/src/routes/app/warehouse/orders/+page.svelte b/resolution-frontend/src/routes/app/warehouse/orders/+page.svelte new file mode 100644 index 0000000..a32e112 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/orders/+page.svelte @@ -0,0 +1,449 @@ + + + + +
+ + +
+ +{#if data.allTags.length > 0} +
+ Filter by tag: +
+ + {#each data.allTags as tag} + + {/each} +
+
+{/if} + +{#if filteredOrders.length === 0} +
+

No orders yet.

+

Orders will appear here once they are created.

+
+{:else} +
+
+ + + + + + + + + + + + + + + + {#each filteredOrders as order (order.id)} + + + + + + + + + + + + {#if expandedOrder === order.id} + + + + {/if} + {/each} + +
RecipientOrdered ByDestinationItemsEst. ShippingStatusTagsCreatedActions
+ {order.firstName} {order.lastName} +
{order.email} +
+ {formatCreatorName(order.createdBy)} +
{order.createdBy.email} +
+ {order.city}, {order.stateProvince} +
{order.country}{order.postalCode ? ` ${order.postalCode}` : ''} +
+ {order.items.length} item{order.items.length !== 1 ? 's' : ''} +
{order.items.reduce((sum: number, oi: any) => sum + oi.quantity, 0)} total qty +
+ {#if order.estimatedShippingCents} + {formatCost(order.estimatedShippingCents)} +
{order.estimatedServiceName || '—'} + {:else} + Not estimated + {/if} +
{statusLabel(order.status)} +
+ {#each order.tags as tagObj} + {tagObj.tag} + {/each} +
+
{new Date(order.createdAt).toLocaleDateString()} + +
+
+
+

Recipient

+

{order.firstName} {order.lastName}

+

{order.email}{order.phone ? ` · ${order.phone}` : ''}

+
+
+

Address

+

{order.addressLine1}

+ {#if order.addressLine2}

{order.addressLine2}

{/if} +

{order.city}, {order.stateProvince} {order.postalCode || ''}

+

{order.country}

+
+
+

Items

+ {#each order.items as oi} +

+ {oi.warehouseItem.name} + {#if oi.sizingChoice}({oi.sizingChoice}){/if} + × {oi.quantity} +

+ {/each} +
+
+

Estimated Package

+ {#if order.estimatedTotalLengthIn} +

Dimensions: {order.estimatedTotalLengthIn}×{order.estimatedTotalWidthIn}{order.estimatedPackageType !== 'flat' ? `×${order.estimatedTotalHeightIn}` : ''} in ({order.estimatedPackageType})

+

Weight: {order.estimatedTotalWeightGrams}g

+ {/if} + {#if order.estimatedShippingCents} +

Shipping: {formatCost(order.estimatedShippingCents)} ({order.estimatedServiceName})

+ {:else} +

Shipping: Not estimated

+ {/if} +
+ {#if order.notes} +
+

Notes

+

{order.notes}

+
+ {/if} +
+
+
+
+{/if} + + diff --git a/resolution-frontend/src/routes/app/warehouse/orders/new/+page.server.ts b/resolution-frontend/src/routes/app/warehouse/orders/new/+page.server.ts new file mode 100644 index 0000000..dbe73d7 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/orders/new/+page.server.ts @@ -0,0 +1,202 @@ +import type { PageServerLoad, Actions } from './$types'; +import { db } from '$lib/server/db'; +import { warehouseItem, warehouseCategory, warehouseOrder, warehouseOrderItem, warehouseOrderTag, ambassadorPathway } from '$lib/server/db/schema'; +import { eq, and, gte, asc, desc, sql, inArray } from 'drizzle-orm'; +import { error, fail, redirect } from '@sveltejs/kit'; +import { createHcbTransfer, getOrgIdForPathway } from '$lib/server/hcb'; + +export const load: PageServerLoad = async ({ parent }) => { + const { user } = await parent(); + + const ambassadorCheck = await db + .select({ userId: ambassadorPathway.userId }) + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, user.id)) + .limit(1); + + const isAmbassador = ambassadorCheck.length > 0; + + if (!user.isAdmin && !isAmbassador) { + throw error(403, 'Access denied'); + } + + const [items, categories, existingTags] = await Promise.all([ + db.select().from(warehouseItem).orderBy(asc(warehouseItem.name)), + db.select().from(warehouseCategory).orderBy(asc(warehouseCategory.sortOrder)), + db.selectDistinct({ tag: warehouseOrderTag.tag }).from(warehouseOrderTag).orderBy(asc(warehouseOrderTag.tag)) + ]); + + return { items, categories, allTags: existingTags.map((t) => t.tag) }; +}; + +export const actions: Actions = { + createOrder: async ({ request, locals }) => { + const user = locals.user; + if (!user) { + return fail(401, { error: 'Not logged in' }); + } + + if (!user.isAdmin) { + const ambassadorCheck = await db + .select({ userId: ambassadorPathway.userId }) + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, user.id)) + .limit(1); + if (ambassadorCheck.length === 0) { + return fail(403, { error: 'Access denied - admin or ambassador only' }); + } + } + + const formData = await request.formData(); + + const firstName = formData.get('firstName') as string; + const lastName = formData.get('lastName') as string; + const email = formData.get('email') as string; + const phone = formData.get('phone') as string; + const addressLine1 = formData.get('addressLine1') as string; + const addressLine2 = formData.get('addressLine2') as string; + const city = formData.get('city') as string; + const stateProvince = formData.get('stateProvince') as string; + const postalCode = formData.get('postalCode') as string; + const country = formData.get('country') as string; + const notes = formData.get('notes') as string; + const tagsString = formData.get('tags') as string; + const estimatedShippingCents = formData.get('estimatedShippingCents') as string; + const estimatedServiceName = formData.get('estimatedServiceName') as string; + const estimatedServiceCode = formData.get('estimatedServiceCode') as string; + + const itemsJson = formData.get('items') as string; + let items: { warehouseItemId: string; quantity: number; sizingChoice?: string }[] = []; + try { + items = JSON.parse(itemsJson || '[]'); + } catch { + return fail(400, { error: 'Invalid items data' }); + } + + if (!firstName || !lastName || !email || !phone || !addressLine1 || !city || !stateProvince || !country) { + return fail(400, { error: 'Missing required fields' }); + } + + if (items.length === 0) { + return fail(400, { error: 'At least one item is required' }); + } + + // Check inventory before creating order + const itemIds = items.map((i) => i.warehouseItemId); + const currentStock = await db + .select({ id: warehouseItem.id, name: warehouseItem.name, quantity: warehouseItem.quantity, costCents: warehouseItem.costCents }) + .from(warehouseItem) + .where(inArray(warehouseItem.id, itemIds)); + + const stockMap = new Map(currentStock.map((s) => [s.id, s])); + for (const item of items) { + const stock = stockMap.get(item.warehouseItemId); + if (!stock || stock.quantity < item.quantity) { + const available = stock?.quantity ?? 0; + const name = stock?.name ?? item.warehouseItemId; + return fail(400, { error: `Insufficient stock for "${name}": ${available} available, ${item.quantity} requested` }); + } + } + + // Use a transaction so order + items + stock decrement are atomic + let orderId: string; + try { + orderId = await db.transaction(async (tx) => { + const [order] = await tx.insert(warehouseOrder).values({ + createdById: user.id, + firstName, + lastName, + email, + phone: phone || null, + addressLine1, + addressLine2: addressLine2 || null, + city, + stateProvince, + postalCode: postalCode || null, + country, + notes: notes || null, + status: 'APPROVED', + estimatedShippingCents: estimatedShippingCents ? parseInt(estimatedShippingCents) : null, + estimatedServiceName: estimatedServiceName || null, + estimatedServiceCode: estimatedServiceCode || null + }).returning({ id: warehouseOrder.id }); + + await Promise.all( + items.map((item) => + tx.insert(warehouseOrderItem).values({ + orderId: order.id, + warehouseItemId: item.warehouseItemId, + quantity: item.quantity, + sizingChoice: item.sizingChoice || null + }) + ) + ); + + for (const item of items) { + const result = await tx.update(warehouseItem) + .set({ quantity: sql`${warehouseItem.quantity} - ${item.quantity}` }) + .where(and(eq(warehouseItem.id, item.warehouseItemId), gte(warehouseItem.quantity, item.quantity))); + if (result.rowCount === 0) { + throw new Error('Insufficient stock (concurrent update)'); + } + } + + if (tagsString && tagsString.trim()) { + const tags = tagsString.split(',').map((t) => t.trim()).filter((t) => t.length > 0); + if (tags.length > 0) { + await tx.insert(warehouseOrderTag).values( + tags.map((tag) => ({ + orderId: order.id, + tag + })) + ); + } + } + + return order.id; + }); + } catch (e: any) { + return fail(409, { error: e.message || 'Order creation failed. Please try again.' }); + } + + // HCB billing: charge the ambassador's pathway org (outside transaction — order is committed) + if (!user.isAdmin) { + const ambassadorPathways = await db + .select({ pathway: ambassadorPathway.pathway }) + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, user.id)) + .limit(1); + + if (ambassadorPathways.length > 0) { + const orgId = getOrgIdForPathway(ambassadorPathways[0].pathway); + if (orgId) { + let itemsTotalCents = 0; + for (const item of items) { + const stock = stockMap.get(item.warehouseItemId); + if (stock) { + itemsTotalCents += stock.costCents * item.quantity; + } + } + const shippingCents = estimatedShippingCents ? parseInt(estimatedShippingCents) : 0; + const totalCents = itemsTotalCents + shippingCents; + + if (totalCents > 0) { + try { + await createHcbTransfer( + orgId, + totalCents, + `Warehouse order #${orderId} by ${firstName} ${lastName}` + ); + } catch (e: any) { + console.error('HCB transfer failed for order', orderId, e.message); + // Order is already committed — log and continue rather than failing the user + // TODO: flag order for manual billing review + } + } + } + } + } + + throw redirect(303, '/app/warehouse/orders'); + } +}; diff --git a/resolution-frontend/src/routes/app/warehouse/orders/new/+page.svelte b/resolution-frontend/src/routes/app/warehouse/orders/new/+page.svelte new file mode 100644 index 0000000..de02551 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/orders/new/+page.svelte @@ -0,0 +1,1217 @@ + + +
+ {#each ['Address', 'Items', 'Shipping', 'Notes', 'Review'] as label, i} + + {#if i < 4} +
i + 1}>
+ {/if} + {/each} +
+ +
{ + return async ({ result, update }) => { + if (result.type === 'failure') { + await update({ reset: false }); + } else { + await update(); + } + }; +}}> + + {#if step === 1} +
+

Recipient Info

+
+ + +
+ + +
+ +
+

Shipping Address

+ + +
+ + +
+
+ + +
+
+ {/if} + + + {#if step === 2} +
+

Select Items

+
+ + {#if searchResults().length > 0} +
    + {#each searchResults() as item (item.id)} +
  • + +
  • + {/each} +
+ {:else if searchQuery.trim().length > 0} +
No items found
+ {/if} +
+ + {#if addedItems().length > 0} +
+ + + + + + + + + + + + {#each addedItems() as item (item.id)} + {@const sizingOptions = item.sizing ? item.sizing.split(',').map((s) => s.trim()) : []} + {@const qty = itemQuantities[item.id] || 0} + + + + + + + + {/each} + +
NameSKUSizingQuantity
{item.name}{item.sku} + {#if sizingOptions.length > 0} + + {:else} + + {/if} + + { itemQuantities[item.id] = parseInt((e.target as HTMLInputElement).value) || 0; }} + /> + + +
+
+ {:else} +

Search above to add items to your order.

+ {/if} +
+ {/if} + + + {#if step === 3} +
+

Choose Shipping

+

⚠️ Shipping costs are estimates and may be adjusted. Final costs will be determined at time of shipment.

+ {#if estimateLoading} +

Estimating shipping costs...

+ {:else if estimateError} +

{estimateError}

+ + {:else if hasEstimated && estimatedRates.length > 0} +
+ {#each estimatedRates as rate} + + {/each} +
+ {:else if hasEstimated} +

No shipping rates available for this destination.

+ {/if} +
+ {/if} + + + {#if step === 4} +
+

Notes & Tags

+ +
+ Tags +
+ {#each tagsArray as tag} + + {tag} + + + {/each} +
+ + {#if tagSuggestions().length > 0} +
    + {#each tagSuggestions() as suggestion} +
  • + +
  • + {/each} +
+ {/if} +
+
+
+
+ {/if} + + + {#if step === 5} +
+

Order Summary

+ +
+

Ship To

+

{firstName} {lastName}

+

{addressLine1}{addressLine2 ? `, ${addressLine2}` : ''}

+

{city}, {stateProvince} {postalCode}

+

{countryName()}

+

{email}{phone ? ` · ${phone}` : ''}

+
+ +
+

Items

+ + + + + + + + + + {#each addedItems() as item} + {@const qty = itemQuantities[item.id] || 0} + + + + + + {/each} + +
ItemQtyCost
+ {item.name} + {#if itemSizing[item.id]} + — {itemSizing[item.id]} + {/if} + {qty}{formatUsd(item.costCents * qty)}
+
+ + {#if selectedRate} +
+

Shipping

+

{selectedRate.serviceName} — ${selectedRate.priceDetails.total.toFixed(2)}

+

Transit: {selectedRate.transitDays} days

+
+ {/if} + + {#if notes || tagsArray.length > 0} +
+

Notes & Tags

+ {#if notes}

{notes}

{/if} + {#if tagsArray.length > 0} +
+ {#each tagsArray as tag} + {tag} + {/each} +
+ {/if} +
+ {/if} + +
+
+ Items + {formatUsd(itemsCostCents())} +
+ {#if selectedRate} +
+ Shipping + ${selectedRate.priceDetails.total.toFixed(2)} +
+
+ Total + {formatUsd(itemsCostCents() + Math.round(selectedRate.priceDetails.total * 100))} +
+ {/if} +
+
+ + + + + + + + + + + + + + + + {#if selectedRate} + + + + {/if} + {/if} + + {#if form?.error} +
{form.error}
+ {/if} + + + +
+ + diff --git a/resolution-frontend/src/routes/app/warehouse/settings/+page.svelte b/resolution-frontend/src/routes/app/warehouse/settings/+page.svelte new file mode 100644 index 0000000..996bd20 --- /dev/null +++ b/resolution-frontend/src/routes/app/warehouse/settings/+page.svelte @@ -0,0 +1,312 @@ + + + + + + +
+

QZ Tray Connection

+ {#if status === 'connecting'} +
+ ⏳ Connecting to QZ Tray... +
+ {:else if status === 'connected'} +
+ ✓ Connected to QZ Tray +
+ {:else} +
+ ✗ Could not connect to QZ Tray + Download QZ Tray +
+ {/if} +
+ +
+

Printer

+
+ +
+ +
+ DPI +
+ {#each [203, 300, 305] as d} + + {/each} +
+
+
+ +
+

Test

+ + {#if testPrintStatus} +

{testPrintStatus}

+ {/if} +
+ +