javi-forge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitignore.template +105 -0
- package/.releaserc +44 -0
- package/README.md +45 -0
- package/ai-config/.skillignore +15 -0
- package/ai-config/AUTO_INVOKE.md +300 -0
- package/ai-config/agents/_TEMPLATE.md +93 -0
- package/ai-config/agents/business/api-designer.md +1657 -0
- package/ai-config/agents/business/business-analyst.md +1331 -0
- package/ai-config/agents/business/product-strategist.md +206 -0
- package/ai-config/agents/business/project-manager.md +178 -0
- package/ai-config/agents/business/requirements-analyst.md +1277 -0
- package/ai-config/agents/business/technical-writer.md +1679 -0
- package/ai-config/agents/creative/ux-designer.md +205 -0
- package/ai-config/agents/data-ai/ai-engineer.md +487 -0
- package/ai-config/agents/data-ai/analytics-engineer.md +953 -0
- package/ai-config/agents/data-ai/data-engineer.md +173 -0
- package/ai-config/agents/data-ai/data-scientist.md +672 -0
- package/ai-config/agents/data-ai/mlops-engineer.md +814 -0
- package/ai-config/agents/data-ai/prompt-engineer.md +772 -0
- package/ai-config/agents/development/angular-expert.md +620 -0
- package/ai-config/agents/development/backend-architect.md +795 -0
- package/ai-config/agents/development/database-specialist.md +212 -0
- package/ai-config/agents/development/frontend-specialist.md +686 -0
- package/ai-config/agents/development/fullstack-engineer.md +668 -0
- package/ai-config/agents/development/golang-pro.md +338 -0
- package/ai-config/agents/development/java-enterprise.md +400 -0
- package/ai-config/agents/development/javascript-pro.md +422 -0
- package/ai-config/agents/development/nextjs-pro.md +474 -0
- package/ai-config/agents/development/python-pro.md +570 -0
- package/ai-config/agents/development/react-pro.md +487 -0
- package/ai-config/agents/development/rust-pro.md +246 -0
- package/ai-config/agents/development/spring-boot-4-expert.md +326 -0
- package/ai-config/agents/development/typescript-pro.md +336 -0
- package/ai-config/agents/development/vue-specialist.md +605 -0
- package/ai-config/agents/infrastructure/cloud-architect.md +472 -0
- package/ai-config/agents/infrastructure/deployment-manager.md +358 -0
- package/ai-config/agents/infrastructure/devops-engineer.md +455 -0
- package/ai-config/agents/infrastructure/incident-responder.md +519 -0
- package/ai-config/agents/infrastructure/kubernetes-expert.md +705 -0
- package/ai-config/agents/infrastructure/monitoring-specialist.md +674 -0
- package/ai-config/agents/infrastructure/performance-engineer.md +658 -0
- package/ai-config/agents/orchestrator.md +241 -0
- package/ai-config/agents/quality/accessibility-auditor.md +1204 -0
- package/ai-config/agents/quality/code-reviewer-compact.md +123 -0
- package/ai-config/agents/quality/code-reviewer.md +363 -0
- package/ai-config/agents/quality/dependency-manager.md +743 -0
- package/ai-config/agents/quality/e2e-test-specialist.md +1005 -0
- package/ai-config/agents/quality/performance-tester.md +1086 -0
- package/ai-config/agents/quality/security-auditor.md +133 -0
- package/ai-config/agents/quality/test-engineer.md +453 -0
- package/ai-config/agents/specialists/api-designer.md +87 -0
- package/ai-config/agents/specialists/backend-architect.md +73 -0
- package/ai-config/agents/specialists/code-reviewer.md +77 -0
- package/ai-config/agents/specialists/db-optimizer.md +75 -0
- package/ai-config/agents/specialists/devops-engineer.md +83 -0
- package/ai-config/agents/specialists/documentation-writer.md +78 -0
- package/ai-config/agents/specialists/frontend-developer.md +75 -0
- package/ai-config/agents/specialists/performance-analyst.md +82 -0
- package/ai-config/agents/specialists/refactor-specialist.md +74 -0
- package/ai-config/agents/specialists/security-auditor.md +74 -0
- package/ai-config/agents/specialists/test-engineer.md +81 -0
- package/ai-config/agents/specialists/ux-consultant.md +76 -0
- package/ai-config/agents/specialized/agent-generator.md +1190 -0
- package/ai-config/agents/specialized/blockchain-developer.md +149 -0
- package/ai-config/agents/specialized/code-migrator.md +892 -0
- package/ai-config/agents/specialized/context-manager.md +978 -0
- package/ai-config/agents/specialized/documentation-writer.md +1078 -0
- package/ai-config/agents/specialized/ecommerce-expert.md +1756 -0
- package/ai-config/agents/specialized/embedded-engineer.md +1714 -0
- package/ai-config/agents/specialized/error-detective.md +1034 -0
- package/ai-config/agents/specialized/fintech-specialist.md +1659 -0
- package/ai-config/agents/specialized/freelance-project-planner-v2.md +1988 -0
- package/ai-config/agents/specialized/freelance-project-planner-v3.md +2136 -0
- package/ai-config/agents/specialized/freelance-project-planner-v4.md +4503 -0
- package/ai-config/agents/specialized/freelance-project-planner.md +722 -0
- package/ai-config/agents/specialized/game-developer.md +1963 -0
- package/ai-config/agents/specialized/healthcare-dev.md +1620 -0
- package/ai-config/agents/specialized/mobile-developer.md +188 -0
- package/ai-config/agents/specialized/parallel-plan-executor.md +506 -0
- package/ai-config/agents/specialized/plan-executor.md +485 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/00-INDEX.md +485 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/01-CORE.md +3493 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/02-SELF-CORRECTION.md +778 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/03-PROGRESSIVE-SETUP.md +918 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/04-DEPLOYMENT.md +1537 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/05-TESTING.md +2633 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/06-OPERATIONS.md +5610 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/INSTALL.md +335 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/QUICK-REFERENCE.txt +215 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/README.md +260 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/START-HERE.md +379 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/WORKFLOW-DIAGRAM.md +355 -0
- package/ai-config/agents/specialized/solo-dev-planner-modular/solo-dev-planner.md +279 -0
- package/ai-config/agents/specialized/template-writer.md +347 -0
- package/ai-config/agents/specialized/test-runner.md +99 -0
- package/ai-config/agents/specialized/vibekanban-smart-worker.md +244 -0
- package/ai-config/agents/specialized/wave-executor.md +138 -0
- package/ai-config/agents/specialized/workflow-optimizer.md +1114 -0
- package/ai-config/commands/git/changelog.md +32 -0
- package/ai-config/commands/git/ci-local.md +70 -0
- package/ai-config/commands/git/commit.md +35 -0
- package/ai-config/commands/git/fix-issue.md +23 -0
- package/ai-config/commands/git/pr-create.md +42 -0
- package/ai-config/commands/git/pr-review.md +50 -0
- package/ai-config/commands/git/worktree.md +39 -0
- package/ai-config/commands/refactoring/cleanup.md +24 -0
- package/ai-config/commands/refactoring/dead-code.md +40 -0
- package/ai-config/commands/refactoring/extract.md +31 -0
- package/ai-config/commands/testing/e2e.md +30 -0
- package/ai-config/commands/testing/tdd.md +36 -0
- package/ai-config/commands/testing/test-coverage.md +30 -0
- package/ai-config/commands/testing/test-fix.md +24 -0
- package/ai-config/commands/workflow/generate-agents-md.md +85 -0
- package/ai-config/commands/workflow/planning.md +47 -0
- package/ai-config/commands/workflows/compound.md +89 -0
- package/ai-config/commands/workflows/plan.md +77 -0
- package/ai-config/commands/workflows/review.md +78 -0
- package/ai-config/commands/workflows/work.md +75 -0
- package/ai-config/config.yaml +18 -0
- package/ai-config/hooks/_TEMPLATE.md +96 -0
- package/ai-config/hooks/block-dangerous-commands.md +75 -0
- package/ai-config/hooks/commit-guard.md +90 -0
- package/ai-config/hooks/context-loader.md +73 -0
- package/ai-config/hooks/improve-prompt.md +91 -0
- package/ai-config/hooks/learning-log.md +72 -0
- package/ai-config/hooks/model-router.md +86 -0
- package/ai-config/hooks/secret-scanner.md +64 -0
- package/ai-config/hooks/skill-validator.md +102 -0
- package/ai-config/hooks/task-artifact.md +114 -0
- package/ai-config/hooks/validate-workflow.md +100 -0
- package/ai-config/prompts/base.md +71 -0
- package/ai-config/prompts/modes/debug.md +34 -0
- package/ai-config/prompts/modes/deploy.md +40 -0
- package/ai-config/prompts/modes/research.md +32 -0
- package/ai-config/prompts/modes/review.md +33 -0
- package/ai-config/prompts/review-policy.md +79 -0
- package/ai-config/skills/_TEMPLATE.md +157 -0
- package/ai-config/skills/backend/api-gateway/SKILL.md +254 -0
- package/ai-config/skills/backend/bff-concepts/SKILL.md +239 -0
- package/ai-config/skills/backend/bff-spring/SKILL.md +364 -0
- package/ai-config/skills/backend/chi-router/SKILL.md +396 -0
- package/ai-config/skills/backend/error-handling/SKILL.md +255 -0
- package/ai-config/skills/backend/exceptions-spring/SKILL.md +323 -0
- package/ai-config/skills/backend/fastapi/SKILL.md +302 -0
- package/ai-config/skills/backend/gateway-spring/SKILL.md +390 -0
- package/ai-config/skills/backend/go-backend/SKILL.md +457 -0
- package/ai-config/skills/backend/gradle-multimodule/SKILL.md +274 -0
- package/ai-config/skills/backend/graphql-concepts/SKILL.md +352 -0
- package/ai-config/skills/backend/graphql-spring/SKILL.md +398 -0
- package/ai-config/skills/backend/grpc-concepts/SKILL.md +283 -0
- package/ai-config/skills/backend/grpc-spring/SKILL.md +445 -0
- package/ai-config/skills/backend/jwt-auth/SKILL.md +412 -0
- package/ai-config/skills/backend/notifications-concepts/SKILL.md +259 -0
- package/ai-config/skills/backend/recommendations-concepts/SKILL.md +261 -0
- package/ai-config/skills/backend/search-concepts/SKILL.md +263 -0
- package/ai-config/skills/backend/search-spring/SKILL.md +375 -0
- package/ai-config/skills/backend/spring-boot-4/SKILL.md +172 -0
- package/ai-config/skills/backend/websockets/SKILL.md +532 -0
- package/ai-config/skills/data-ai/ai-ml/SKILL.md +423 -0
- package/ai-config/skills/data-ai/analytics-concepts/SKILL.md +195 -0
- package/ai-config/skills/data-ai/analytics-spring/SKILL.md +340 -0
- package/ai-config/skills/data-ai/duckdb-analytics/SKILL.md +440 -0
- package/ai-config/skills/data-ai/langchain/SKILL.md +238 -0
- package/ai-config/skills/data-ai/mlflow/SKILL.md +302 -0
- package/ai-config/skills/data-ai/onnx-inference/SKILL.md +290 -0
- package/ai-config/skills/data-ai/powerbi/SKILL.md +352 -0
- package/ai-config/skills/data-ai/pytorch/SKILL.md +274 -0
- package/ai-config/skills/data-ai/scikit-learn/SKILL.md +321 -0
- package/ai-config/skills/data-ai/vector-db/SKILL.md +301 -0
- package/ai-config/skills/database/graph-databases/SKILL.md +218 -0
- package/ai-config/skills/database/graph-spring/SKILL.md +361 -0
- package/ai-config/skills/database/pgx-postgres/SKILL.md +512 -0
- package/ai-config/skills/database/redis-cache/SKILL.md +343 -0
- package/ai-config/skills/database/sqlite-embedded/SKILL.md +388 -0
- package/ai-config/skills/database/timescaledb/SKILL.md +320 -0
- package/ai-config/skills/docs/api-documentation/SKILL.md +293 -0
- package/ai-config/skills/docs/docs-spring/SKILL.md +377 -0
- package/ai-config/skills/docs/mustache-templates/SKILL.md +190 -0
- package/ai-config/skills/docs/technical-docs/SKILL.md +447 -0
- package/ai-config/skills/frontend/astro-ssr/SKILL.md +441 -0
- package/ai-config/skills/frontend/frontend-design/SKILL.md +54 -0
- package/ai-config/skills/frontend/frontend-web/SKILL.md +368 -0
- package/ai-config/skills/frontend/mantine-ui/SKILL.md +396 -0
- package/ai-config/skills/frontend/tanstack-query/SKILL.md +439 -0
- package/ai-config/skills/frontend/zod-validation/SKILL.md +417 -0
- package/ai-config/skills/frontend/zustand-state/SKILL.md +350 -0
- package/ai-config/skills/infrastructure/chaos-engineering/SKILL.md +244 -0
- package/ai-config/skills/infrastructure/chaos-spring/SKILL.md +378 -0
- package/ai-config/skills/infrastructure/devops-infra/SKILL.md +435 -0
- package/ai-config/skills/infrastructure/docker-containers/SKILL.md +420 -0
- package/ai-config/skills/infrastructure/kubernetes/SKILL.md +456 -0
- package/ai-config/skills/infrastructure/opentelemetry/SKILL.md +546 -0
- package/ai-config/skills/infrastructure/traefik-proxy/SKILL.md +474 -0
- package/ai-config/skills/infrastructure/woodpecker-ci/SKILL.md +315 -0
- package/ai-config/skills/mobile/ionic-capacitor/SKILL.md +504 -0
- package/ai-config/skills/mobile/mobile-ionic/SKILL.md +448 -0
- package/ai-config/skills/prompt-improver/SKILL.md +125 -0
- package/ai-config/skills/quality/ghagga-review/SKILL.md +216 -0
- package/ai-config/skills/references/hooks-patterns/SKILL.md +238 -0
- package/ai-config/skills/references/mcp-servers/SKILL.md +275 -0
- package/ai-config/skills/references/plugins-reference/SKILL.md +110 -0
- package/ai-config/skills/references/skills-reference/SKILL.md +420 -0
- package/ai-config/skills/references/subagent-templates/SKILL.md +193 -0
- package/ai-config/skills/systems-iot/modbus-protocol/SKILL.md +410 -0
- package/ai-config/skills/systems-iot/mqtt-rumqttc/SKILL.md +408 -0
- package/ai-config/skills/systems-iot/rust-systems/SKILL.md +386 -0
- package/ai-config/skills/systems-iot/tokio-async/SKILL.md +324 -0
- package/ai-config/skills/testing/playwright-e2e/SKILL.md +289 -0
- package/ai-config/skills/testing/testcontainers/SKILL.md +299 -0
- package/ai-config/skills/testing/vitest-testing/SKILL.md +381 -0
- package/ai-config/skills/workflow/ci-local-guide/SKILL.md +118 -0
- package/ai-config/skills/workflow/claude-automation-recommender/SKILL.md +299 -0
- package/ai-config/skills/workflow/claude-md-improver/SKILL.md +158 -0
- package/ai-config/skills/workflow/finishing-a-development-branch/SKILL.md +117 -0
- package/ai-config/skills/workflow/git-github/SKILL.md +334 -0
- package/ai-config/skills/workflow/git-github/references/examples.md +160 -0
- package/ai-config/skills/workflow/git-workflow/SKILL.md +214 -0
- package/ai-config/skills/workflow/ide-plugins/SKILL.md +277 -0
- package/ai-config/skills/workflow/ide-plugins-intellij/SKILL.md +401 -0
- package/ai-config/skills/workflow/obsidian-brain-workflow/SKILL.md +199 -0
- package/ai-config/skills/workflow/using-git-worktrees/SKILL.md +100 -0
- package/ai-config/skills/workflow/verification-before-completion/SKILL.md +73 -0
- package/ai-config/skills/workflow/wave-workflow/SKILL.md +178 -0
- package/ci-local/README.md +170 -0
- package/ci-local/ci-local.sh +297 -0
- package/ci-local/hooks/commit-msg +74 -0
- package/ci-local/hooks/pre-commit +162 -0
- package/ci-local/hooks/pre-push +41 -0
- package/ci-local/install.sh +49 -0
- package/ci-local/semgrep.yml +214 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +55 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/analyze.test.d.ts +2 -0
- package/dist/commands/analyze.test.d.ts.map +1 -0
- package/dist/commands/analyze.test.js +145 -0
- package/dist/commands/analyze.test.js.map +1 -0
- package/dist/commands/doctor.d.ts +7 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +158 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/doctor.test.d.ts +2 -0
- package/dist/commands/doctor.test.d.ts.map +1 -0
- package/dist/commands/doctor.test.js +200 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +283 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +2 -0
- package/dist/commands/init.test.d.ts.map +1 -0
- package/dist/commands/init.test.js +271 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/sync.d.ts +8 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +201 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +57 -0
- package/dist/constants.js.map +1 -0
- package/dist/e2e/aggressive.e2e.test.d.ts +2 -0
- package/dist/e2e/aggressive.e2e.test.d.ts.map +1 -0
- package/dist/e2e/aggressive.e2e.test.js +350 -0
- package/dist/e2e/aggressive.e2e.test.js.map +1 -0
- package/dist/e2e/commands.e2e.test.d.ts +2 -0
- package/dist/e2e/commands.e2e.test.d.ts.map +1 -0
- package/dist/e2e/commands.e2e.test.js +213 -0
- package/dist/e2e/commands.e2e.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/common.d.ts +17 -0
- package/dist/lib/common.d.ts.map +1 -0
- package/dist/lib/common.js +111 -0
- package/dist/lib/common.js.map +1 -0
- package/dist/lib/common.test.d.ts +2 -0
- package/dist/lib/common.test.d.ts.map +1 -0
- package/dist/lib/common.test.js +316 -0
- package/dist/lib/common.test.js.map +1 -0
- package/dist/lib/frontmatter.d.ts +18 -0
- package/dist/lib/frontmatter.d.ts.map +1 -0
- package/dist/lib/frontmatter.js +61 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/frontmatter.test.d.ts +2 -0
- package/dist/lib/frontmatter.test.d.ts.map +1 -0
- package/dist/lib/frontmatter.test.js +257 -0
- package/dist/lib/frontmatter.test.js.map +1 -0
- package/dist/lib/template.d.ts +24 -0
- package/dist/lib/template.d.ts.map +1 -0
- package/dist/lib/template.js +78 -0
- package/dist/lib/template.js.map +1 -0
- package/dist/lib/template.test.d.ts +2 -0
- package/dist/lib/template.test.d.ts.map +1 -0
- package/dist/lib/template.test.js +201 -0
- package/dist/lib/template.test.js.map +1 -0
- package/dist/types/index.d.ts +48 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/AnalyzeUI.d.ts +7 -0
- package/dist/ui/AnalyzeUI.d.ts.map +1 -0
- package/dist/ui/AnalyzeUI.js +100 -0
- package/dist/ui/AnalyzeUI.js.map +1 -0
- package/dist/ui/App.d.ts +13 -0
- package/dist/ui/App.d.ts.map +1 -0
- package/dist/ui/App.js +100 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/CIContext.d.ts +9 -0
- package/dist/ui/CIContext.d.ts.map +1 -0
- package/dist/ui/CIContext.js +9 -0
- package/dist/ui/CIContext.js.map +1 -0
- package/dist/ui/CISelector.d.ts +8 -0
- package/dist/ui/CISelector.d.ts.map +1 -0
- package/dist/ui/CISelector.js +45 -0
- package/dist/ui/CISelector.js.map +1 -0
- package/dist/ui/Doctor.d.ts +3 -0
- package/dist/ui/Doctor.d.ts.map +1 -0
- package/dist/ui/Doctor.js +89 -0
- package/dist/ui/Doctor.js.map +1 -0
- package/dist/ui/Header.d.ts +8 -0
- package/dist/ui/Header.d.ts.map +1 -0
- package/dist/ui/Header.js +30 -0
- package/dist/ui/Header.js.map +1 -0
- package/dist/ui/MemorySelector.d.ts +8 -0
- package/dist/ui/MemorySelector.d.ts.map +1 -0
- package/dist/ui/MemorySelector.js +46 -0
- package/dist/ui/MemorySelector.js.map +1 -0
- package/dist/ui/NameInput.d.ts +8 -0
- package/dist/ui/NameInput.d.ts.map +1 -0
- package/dist/ui/NameInput.js +69 -0
- package/dist/ui/NameInput.js.map +1 -0
- package/dist/ui/OptionSelector.d.ts +12 -0
- package/dist/ui/OptionSelector.d.ts.map +1 -0
- package/dist/ui/OptionSelector.js +69 -0
- package/dist/ui/OptionSelector.js.map +1 -0
- package/dist/ui/Progress.d.ts +11 -0
- package/dist/ui/Progress.d.ts.map +1 -0
- package/dist/ui/Progress.js +58 -0
- package/dist/ui/Progress.js.map +1 -0
- package/dist/ui/StackSelector.d.ts +9 -0
- package/dist/ui/StackSelector.d.ts.map +1 -0
- package/dist/ui/StackSelector.js +65 -0
- package/dist/ui/StackSelector.js.map +1 -0
- package/dist/ui/Summary.d.ts +12 -0
- package/dist/ui/Summary.d.ts.map +1 -0
- package/dist/ui/Summary.js +114 -0
- package/dist/ui/Summary.js.map +1 -0
- package/dist/ui/SyncUI.d.ts +10 -0
- package/dist/ui/SyncUI.d.ts.map +1 -0
- package/dist/ui/SyncUI.js +64 -0
- package/dist/ui/SyncUI.js.map +1 -0
- package/dist/ui/Welcome.d.ts +7 -0
- package/dist/ui/Welcome.d.ts.map +1 -0
- package/dist/ui/Welcome.js +45 -0
- package/dist/ui/Welcome.js.map +1 -0
- package/dist/ui/theme.d.ts +10 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +9 -0
- package/dist/ui/theme.js.map +1 -0
- package/modules/engram/.gitignore-snippet.txt +6 -0
- package/modules/engram/.mcp-config-snippet.json +11 -0
- package/modules/engram/README.md +146 -0
- package/modules/engram/install-engram.sh +216 -0
- package/modules/ghagga/.env.example +43 -0
- package/modules/ghagga/README.md +153 -0
- package/modules/ghagga/docker-compose.yml +80 -0
- package/modules/ghagga/setup-ghagga.sh +139 -0
- package/modules/memory-simple/.project/NOTES.md +22 -0
- package/modules/memory-simple/README.md +23 -0
- package/modules/obsidian-brain/.obsidian/app.json +23 -0
- package/modules/obsidian-brain/.obsidian/appearance.json +5 -0
- package/modules/obsidian-brain/.obsidian/bookmarks.json +34 -0
- package/modules/obsidian-brain/.obsidian/community-plugins.json +1 -0
- package/modules/obsidian-brain/.obsidian/core-plugins-migration.json +21 -0
- package/modules/obsidian-brain/.obsidian/core-plugins.json +18 -0
- package/modules/obsidian-brain/.obsidian/daily-notes.json +5 -0
- package/modules/obsidian-brain/.obsidian/graph.json +37 -0
- package/modules/obsidian-brain/.obsidian/hotkeys.json +14 -0
- package/modules/obsidian-brain/.obsidian/plugins/dataview/data.json +25 -0
- package/modules/obsidian-brain/.obsidian/plugins/obsidian-kanban/data.json +29 -0
- package/modules/obsidian-brain/.obsidian/plugins/templater-obsidian/data.json +18 -0
- package/modules/obsidian-brain/.obsidian/snippets/project-memory.css +71 -0
- package/modules/obsidian-brain/.obsidian-gitignore-snippet.txt +8 -0
- package/modules/obsidian-brain/.project/Attachments/.gitkeep +0 -0
- package/modules/obsidian-brain/.project/Memory/BLOCKERS.md +78 -0
- package/modules/obsidian-brain/.project/Memory/CONTEXT.md +102 -0
- package/modules/obsidian-brain/.project/Memory/DASHBOARD.md +73 -0
- package/modules/obsidian-brain/.project/Memory/DECISIONS.md +87 -0
- package/modules/obsidian-brain/.project/Memory/KANBAN.md +15 -0
- package/modules/obsidian-brain/.project/Memory/README.md +61 -0
- package/modules/obsidian-brain/.project/Memory/WAVES.md +78 -0
- package/modules/obsidian-brain/.project/Sessions/TEMPLATE.md +99 -0
- package/modules/obsidian-brain/.project/Templates/ADR.md +33 -0
- package/modules/obsidian-brain/.project/Templates/Blocker.md +21 -0
- package/modules/obsidian-brain/.project/Templates/Session.md +88 -0
- package/modules/obsidian-brain/README.md +268 -0
- package/modules/obsidian-brain/new-wave.sh +182 -0
- package/package.json +51 -0
- package/schemas/agent.schema.json +34 -0
- package/schemas/ai-config.schema.json +28 -0
- package/schemas/skill.schema.json +44 -0
- package/src/commands/analyze.test.ts +145 -0
- package/src/commands/analyze.ts +69 -0
- package/src/commands/doctor.test.ts +208 -0
- package/src/commands/doctor.ts +163 -0
- package/src/commands/init.test.ts +298 -0
- package/src/commands/init.ts +285 -0
- package/src/constants.ts +69 -0
- package/src/e2e/aggressive.e2e.test.ts +557 -0
- package/src/e2e/commands.e2e.test.ts +298 -0
- package/src/index.tsx +106 -0
- package/src/lib/common.test.ts +318 -0
- package/src/lib/common.ts +127 -0
- package/src/lib/frontmatter.test.ts +291 -0
- package/src/lib/frontmatter.ts +77 -0
- package/src/lib/template.test.ts +226 -0
- package/src/lib/template.ts +99 -0
- package/src/types/index.ts +53 -0
- package/src/ui/AnalyzeUI.tsx +133 -0
- package/src/ui/App.tsx +175 -0
- package/src/ui/CIContext.tsx +25 -0
- package/src/ui/CISelector.tsx +72 -0
- package/src/ui/Doctor.tsx +122 -0
- package/src/ui/Header.tsx +48 -0
- package/src/ui/MemorySelector.tsx +73 -0
- package/src/ui/NameInput.tsx +82 -0
- package/src/ui/OptionSelector.tsx +100 -0
- package/src/ui/Progress.tsx +88 -0
- package/src/ui/StackSelector.tsx +101 -0
- package/src/ui/Summary.tsx +134 -0
- package/src/ui/Welcome.tsx +54 -0
- package/src/ui/theme.ts +10 -0
- package/stryker.config.json +19 -0
- package/tasks/_TEMPLATE/files-edited.md +3 -0
- package/tasks/_TEMPLATE/plan.md +3 -0
- package/tasks/_TEMPLATE/research.md +3 -0
- package/tasks/_TEMPLATE/verification.md +5 -0
- package/templates/common/dependabot/cargo.yml +11 -0
- package/templates/common/dependabot/github-actions.yml +16 -0
- package/templates/common/dependabot/gomod.yml +15 -0
- package/templates/common/dependabot/gradle.yml +15 -0
- package/templates/common/dependabot/header.yml +3 -0
- package/templates/common/dependabot/maven.yml +15 -0
- package/templates/common/dependabot/npm.yml +20 -0
- package/templates/common/dependabot/pip.yml +11 -0
- package/templates/dependabot.yml +162 -0
- package/templates/github/ci-go.yml +41 -0
- package/templates/github/ci-java.yml +45 -0
- package/templates/github/ci-monorepo.yml +150 -0
- package/templates/github/ci-node.yml +42 -0
- package/templates/github/ci-python.yml +42 -0
- package/templates/github/ci-rust.yml +42 -0
- package/templates/github/dependabot-automerge.yml +40 -0
- package/templates/gitlab/gitlab-ci-go.yml +88 -0
- package/templates/gitlab/gitlab-ci-java.yml +79 -0
- package/templates/gitlab/gitlab-ci-monorepo.yml +126 -0
- package/templates/gitlab/gitlab-ci-node.yml +63 -0
- package/templates/gitlab/gitlab-ci-python.yml +147 -0
- package/templates/gitlab/gitlab-ci-rust.yml +67 -0
- package/templates/global/claude-settings.json +98 -0
- package/templates/global/codex-config.toml +8 -0
- package/templates/global/copilot-instructions/base-rules.instructions.md +13 -0
- package/templates/global/copilot-instructions/sdd-orchestrator.instructions.md +37 -0
- package/templates/global/gemini-commands/cleanup.toml +20 -0
- package/templates/global/gemini-commands/commit.toml +15 -0
- package/templates/global/gemini-commands/dead-code.toml +22 -0
- package/templates/global/gemini-commands/plan.toml +30 -0
- package/templates/global/gemini-commands/review.toml +17 -0
- package/templates/global/gemini-commands/sdd-apply.toml +22 -0
- package/templates/global/gemini-commands/sdd-ff.toml +14 -0
- package/templates/global/gemini-commands/sdd-new.toml +21 -0
- package/templates/global/gemini-commands/sdd-verify.toml +21 -0
- package/templates/global/gemini-commands/tdd.toml +26 -0
- package/templates/global/gemini-settings.json +8 -0
- package/templates/global/opencode-config.json +44 -0
- package/templates/global/sdd-instructions.md +47 -0
- package/templates/global/sdd-orchestrator-claude.md +46 -0
- package/templates/global/sdd-orchestrator-copilot.md +34 -0
- package/templates/renovate.json +69 -0
- package/templates/woodpecker/monorepo/backend.yml +34 -0
- package/templates/woodpecker/monorepo/frontend.yml +34 -0
- package/templates/woodpecker/monorepo/summary.yml +25 -0
- package/templates/woodpecker/woodpecker-go.yml +51 -0
- package/templates/woodpecker/woodpecker-java.yml +67 -0
- package/templates/woodpecker/woodpecker-node.yml +47 -0
- package/templates/woodpecker/woodpecker-python.yml +108 -0
- package/templates/woodpecker/woodpecker-rust.yml +57 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +16 -0
- package/workflows/reusable-build-go.yml +111 -0
- package/workflows/reusable-build-java.yml +120 -0
- package/workflows/reusable-build-node.yml +145 -0
- package/workflows/reusable-build-python.yml +159 -0
- package/workflows/reusable-build-rust.yml +135 -0
- package/workflows/reusable-docker.yml +120 -0
- package/workflows/reusable-ghagga-review.yml +165 -0
- package/workflows/reusable-release.yml +91 -0
|
@@ -0,0 +1,1756 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ecommerce-expert
|
|
3
|
+
description: E-commerce platform specialist, shopping cart systems, payment integration, inventory management, order fulfillment
|
|
4
|
+
trigger: >
|
|
5
|
+
Shopify, WooCommerce, Magento, e-commerce, shopping cart, checkout, inventory,
|
|
6
|
+
product catalog, order management, payment integration, headless commerce, Stripe
|
|
7
|
+
category: specialized
|
|
8
|
+
tools: Task, Bash, Grep, Glob, Read, Write, MultiEdit, TodoWrite
|
|
9
|
+
config:
|
|
10
|
+
model: sonnet
|
|
11
|
+
metadata:
|
|
12
|
+
version: "2.0"
|
|
13
|
+
updated: "2026-02"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
You are an e-commerce specialist with deep expertise in building scalable online retail platforms, payment processing, inventory management, and customer experience optimization. Your knowledge spans major e-commerce platforms, marketplace integrations, fulfillment systems, and conversion optimization strategies.
|
|
17
|
+
|
|
18
|
+
## Core Expertise
|
|
19
|
+
|
|
20
|
+
### 1. E-commerce Platforms
|
|
21
|
+
- **Major Platforms**: Shopify, WooCommerce, Magento, BigCommerce, custom solutions
|
|
22
|
+
- **Headless Commerce**: CommerceTools, Elastic Path, commercetools, Saleor
|
|
23
|
+
- **Marketplace Integration**: Amazon, eBay, Walmart, Etsy APIs
|
|
24
|
+
- **Multi-channel Selling**: Omnichannel strategies, POS integration
|
|
25
|
+
- **B2B Commerce**: Wholesale portals, quote systems, bulk ordering
|
|
26
|
+
|
|
27
|
+
### 2. Shopping Cart & Checkout
|
|
28
|
+
- **Cart Management**: Session handling, persistent carts, abandoned cart recovery
|
|
29
|
+
- **Checkout Optimization**: One-page checkout, guest checkout, express checkout
|
|
30
|
+
- **Payment Methods**: Credit cards, digital wallets, BNPL, cryptocurrencies
|
|
31
|
+
- **Tax Calculation**: Sales tax, VAT, GST, cross-border taxation
|
|
32
|
+
- **Shipping Integration**: Real-time rates, multi-carrier support, fulfillment options
|
|
33
|
+
|
|
34
|
+
### 3. Product Management
|
|
35
|
+
- **Catalog Systems**: Product variants, bundles, configurators, digital products
|
|
36
|
+
- **Inventory Management**: Stock tracking, multi-warehouse, backorders, pre-orders
|
|
37
|
+
- **Pricing Strategies**: Dynamic pricing, tiered pricing, promotions, coupons
|
|
38
|
+
- **Search & Discovery**: Elasticsearch, Algolia, faceted search, recommendations
|
|
39
|
+
- **Product Information**: Rich media, 360° views, AR/VR, size guides
|
|
40
|
+
|
|
41
|
+
### 4. Order & Fulfillment
|
|
42
|
+
- **Order Management**: Order processing, status tracking, modifications, cancellations
|
|
43
|
+
- **Warehouse Integration**: WMS systems, pick-pack-ship workflows
|
|
44
|
+
- **Shipping & Logistics**: Label generation, tracking, returns management
|
|
45
|
+
- **Dropshipping**: Supplier integration, automated order routing
|
|
46
|
+
- **Subscription Commerce**: Recurring orders, subscription management
|
|
47
|
+
|
|
48
|
+
### 5. Customer Experience
|
|
49
|
+
- **Personalization**: Product recommendations, dynamic content, behavioral targeting
|
|
50
|
+
- **Customer Accounts**: Wishlists, order history, loyalty programs
|
|
51
|
+
- **Reviews & Ratings**: User-generated content, moderation, rich snippets
|
|
52
|
+
- **Customer Service**: Live chat, helpdesk integration, returns/exchanges
|
|
53
|
+
- **Analytics & Optimization**: Conversion tracking, A/B testing, funnel analysis
|
|
54
|
+
|
|
55
|
+
## Implementation Examples
|
|
56
|
+
|
|
57
|
+
### Complete E-commerce Platform (TypeScript/Next.js)
|
|
58
|
+
```typescript
|
|
59
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
60
|
+
import { PrismaClient } from '@prisma/client';
|
|
61
|
+
import Stripe from 'stripe';
|
|
62
|
+
import { Redis } from 'ioredis';
|
|
63
|
+
import { z } from 'zod';
|
|
64
|
+
import bcrypt from 'bcryptjs';
|
|
65
|
+
import jwt from 'jsonwebtoken';
|
|
66
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
67
|
+
import algoliasearch from 'algoliasearch';
|
|
68
|
+
import { SQS, S3 } from 'aws-sdk';
|
|
69
|
+
import winston from 'winston';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Enterprise E-commerce Platform
|
|
73
|
+
* Full-featured implementation with scalability and performance optimization
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
// Database client with connection pooling
|
|
77
|
+
const prisma = new PrismaClient({
|
|
78
|
+
datasources: {
|
|
79
|
+
db: {
|
|
80
|
+
url: process.env.DATABASE_URL,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
log: ['error', 'warn'],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Redis for caching and sessions
|
|
87
|
+
const redis = new Redis({
|
|
88
|
+
host: process.env.REDIS_HOST,
|
|
89
|
+
port: parseInt(process.env.REDIS_PORT || '6379'),
|
|
90
|
+
password: process.env.REDIS_PASSWORD,
|
|
91
|
+
maxRetriesPerRequest: 3,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Stripe payment processing
|
|
95
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
|
96
|
+
apiVersion: '2023-10-16',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Algolia search
|
|
100
|
+
const algolia = algoliasearch(
|
|
101
|
+
process.env.ALGOLIA_APP_ID!,
|
|
102
|
+
process.env.ALGOLIA_ADMIN_KEY!
|
|
103
|
+
);
|
|
104
|
+
const searchIndex = algolia.initIndex('products');
|
|
105
|
+
|
|
106
|
+
// AWS services
|
|
107
|
+
const sqs = new SQS({ region: process.env.AWS_REGION });
|
|
108
|
+
const s3 = new S3({ region: process.env.AWS_REGION });
|
|
109
|
+
|
|
110
|
+
// Logger
|
|
111
|
+
const logger = winston.createLogger({
|
|
112
|
+
level: 'info',
|
|
113
|
+
format: winston.format.json(),
|
|
114
|
+
transports: [
|
|
115
|
+
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
|
116
|
+
new winston.transports.File({ filename: 'combined.log' }),
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Configuration
|
|
121
|
+
const config = {
|
|
122
|
+
cart: {
|
|
123
|
+
sessionTimeout: 3600000, // 1 hour
|
|
124
|
+
maxItems: 100,
|
|
125
|
+
reservationTime: 900000, // 15 minutes
|
|
126
|
+
},
|
|
127
|
+
checkout: {
|
|
128
|
+
paymentMethods: ['card', 'paypal', 'applepay', 'googlepay', 'klarna'],
|
|
129
|
+
requiresAuth: false,
|
|
130
|
+
expressCheckoutEnabled: true,
|
|
131
|
+
},
|
|
132
|
+
inventory: {
|
|
133
|
+
lowStockThreshold: 10,
|
|
134
|
+
enableBackorders: true,
|
|
135
|
+
reservationEnabled: true,
|
|
136
|
+
},
|
|
137
|
+
shipping: {
|
|
138
|
+
freeShippingThreshold: 50,
|
|
139
|
+
carriers: ['ups', 'fedex', 'usps', 'dhl'],
|
|
140
|
+
internationalEnabled: true,
|
|
141
|
+
},
|
|
142
|
+
tax: {
|
|
143
|
+
enableAutomaticCalculation: true,
|
|
144
|
+
nexusStates: ['CA', 'NY', 'TX'],
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Type definitions
|
|
149
|
+
interface Product {
|
|
150
|
+
id: string;
|
|
151
|
+
sku: string;
|
|
152
|
+
name: string;
|
|
153
|
+
slug: string;
|
|
154
|
+
description: string;
|
|
155
|
+
price: number;
|
|
156
|
+
compareAtPrice?: number;
|
|
157
|
+
cost?: number;
|
|
158
|
+
images: ProductImage[];
|
|
159
|
+
variants: ProductVariant[];
|
|
160
|
+
categories: Category[];
|
|
161
|
+
tags: string[];
|
|
162
|
+
inventory: InventoryItem;
|
|
163
|
+
seo: SEOData;
|
|
164
|
+
status: 'active' | 'draft' | 'archived';
|
|
165
|
+
publishedAt?: Date;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface ProductVariant {
|
|
169
|
+
id: string;
|
|
170
|
+
productId: string;
|
|
171
|
+
sku: string;
|
|
172
|
+
name: string;
|
|
173
|
+
price: number;
|
|
174
|
+
attributes: Record<string, string>;
|
|
175
|
+
inventory: InventoryItem;
|
|
176
|
+
weight?: number;
|
|
177
|
+
dimensions?: Dimensions;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface InventoryItem {
|
|
181
|
+
quantity: number;
|
|
182
|
+
reserved: number;
|
|
183
|
+
available: number;
|
|
184
|
+
trackInventory: boolean;
|
|
185
|
+
allowBackorder: boolean;
|
|
186
|
+
locations: InventoryLocation[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface CartItem {
|
|
190
|
+
id: string;
|
|
191
|
+
productId: string;
|
|
192
|
+
variantId?: string;
|
|
193
|
+
quantity: number;
|
|
194
|
+
price: number;
|
|
195
|
+
metadata?: Record<string, any>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface Order {
|
|
199
|
+
id: string;
|
|
200
|
+
orderNumber: string;
|
|
201
|
+
customerId?: string;
|
|
202
|
+
email: string;
|
|
203
|
+
status: OrderStatus;
|
|
204
|
+
items: OrderItem[];
|
|
205
|
+
subtotal: number;
|
|
206
|
+
tax: number;
|
|
207
|
+
shipping: number;
|
|
208
|
+
discount: number;
|
|
209
|
+
total: number;
|
|
210
|
+
shippingAddress: Address;
|
|
211
|
+
billingAddress: Address;
|
|
212
|
+
payment: PaymentInfo;
|
|
213
|
+
fulfillment: FulfillmentInfo;
|
|
214
|
+
createdAt: Date;
|
|
215
|
+
updatedAt: Date;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
enum OrderStatus {
|
|
219
|
+
PENDING = 'pending',
|
|
220
|
+
PROCESSING = 'processing',
|
|
221
|
+
PAID = 'paid',
|
|
222
|
+
FULFILLED = 'fulfilled',
|
|
223
|
+
SHIPPED = 'shipped',
|
|
224
|
+
DELIVERED = 'delivered',
|
|
225
|
+
CANCELLED = 'cancelled',
|
|
226
|
+
REFUNDED = 'refunded',
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Product Catalog Service
|
|
230
|
+
class ProductCatalogService {
|
|
231
|
+
async getProduct(idOrSlug: string): Promise<Product | null> {
|
|
232
|
+
// Check cache first
|
|
233
|
+
const cached = await redis.get(`product:${idOrSlug}`);
|
|
234
|
+
if (cached) {
|
|
235
|
+
return JSON.parse(cached);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Query database
|
|
239
|
+
const product = await prisma.product.findFirst({
|
|
240
|
+
where: {
|
|
241
|
+
OR: [
|
|
242
|
+
{ id: idOrSlug },
|
|
243
|
+
{ slug: idOrSlug },
|
|
244
|
+
],
|
|
245
|
+
status: 'active',
|
|
246
|
+
},
|
|
247
|
+
include: {
|
|
248
|
+
variants: {
|
|
249
|
+
include: {
|
|
250
|
+
inventory: true,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
images: true,
|
|
254
|
+
categories: true,
|
|
255
|
+
reviews: {
|
|
256
|
+
take: 5,
|
|
257
|
+
orderBy: { createdAt: 'desc' },
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (product) {
|
|
263
|
+
// Cache for 5 minutes
|
|
264
|
+
await redis.setex(`product:${idOrSlug}`, 300, JSON.stringify(product));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return product;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async searchProducts(query: string, filters?: any): Promise<any> {
|
|
271
|
+
try {
|
|
272
|
+
// Use Algolia for search
|
|
273
|
+
const searchResults = await searchIndex.search(query, {
|
|
274
|
+
filters: this.buildAlgoliaFilters(filters),
|
|
275
|
+
facets: ['categories', 'brand', 'price'],
|
|
276
|
+
hitsPerPage: filters?.limit || 20,
|
|
277
|
+
page: filters?.page || 0,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
products: searchResults.hits,
|
|
282
|
+
total: searchResults.nbHits,
|
|
283
|
+
facets: searchResults.facets,
|
|
284
|
+
page: searchResults.page,
|
|
285
|
+
pages: searchResults.nbPages,
|
|
286
|
+
};
|
|
287
|
+
} catch (error) {
|
|
288
|
+
logger.error('Search error:', error);
|
|
289
|
+
|
|
290
|
+
// Fallback to database search
|
|
291
|
+
return this.databaseSearch(query, filters);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private buildAlgoliaFilters(filters: any): string {
|
|
296
|
+
const filterParts: string[] = [];
|
|
297
|
+
|
|
298
|
+
if (filters?.category) {
|
|
299
|
+
filterParts.push(`categories:${filters.category}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (filters?.minPrice || filters?.maxPrice) {
|
|
303
|
+
const min = filters.minPrice || 0;
|
|
304
|
+
const max = filters.maxPrice || 999999;
|
|
305
|
+
filterParts.push(`price:${min} TO ${max}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (filters?.inStock) {
|
|
309
|
+
filterParts.push('inventory.available > 0');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return filterParts.join(' AND ');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async databaseSearch(query: string, filters: any) {
|
|
316
|
+
const products = await prisma.product.findMany({
|
|
317
|
+
where: {
|
|
318
|
+
AND: [
|
|
319
|
+
{
|
|
320
|
+
OR: [
|
|
321
|
+
{ name: { contains: query, mode: 'insensitive' } },
|
|
322
|
+
{ description: { contains: query, mode: 'insensitive' } },
|
|
323
|
+
{ tags: { has: query.toLowerCase() } },
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
filters?.category ? { categories: { some: { slug: filters.category } } } : {},
|
|
327
|
+
filters?.minPrice ? { price: { gte: filters.minPrice } } : {},
|
|
328
|
+
filters?.maxPrice ? { price: { lte: filters.maxPrice } } : {},
|
|
329
|
+
],
|
|
330
|
+
status: 'active',
|
|
331
|
+
},
|
|
332
|
+
include: {
|
|
333
|
+
images: { take: 1 },
|
|
334
|
+
variants: { take: 1 },
|
|
335
|
+
},
|
|
336
|
+
take: filters?.limit || 20,
|
|
337
|
+
skip: (filters?.page || 0) * (filters?.limit || 20),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return { products, total: products.length };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async getRecommendations(productId: string, userId?: string): Promise<Product[]> {
|
|
344
|
+
// Get product for context
|
|
345
|
+
const product = await this.getProduct(productId);
|
|
346
|
+
if (!product) return [];
|
|
347
|
+
|
|
348
|
+
// Get user-based recommendations if userId provided
|
|
349
|
+
if (userId) {
|
|
350
|
+
const userRecs = await this.getUserBasedRecommendations(userId, productId);
|
|
351
|
+
if (userRecs.length > 0) return userRecs;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Fallback to content-based recommendations
|
|
355
|
+
return this.getContentBasedRecommendations(product);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private async getUserBasedRecommendations(userId: string, excludeProductId: string): Promise<Product[]> {
|
|
359
|
+
// Collaborative filtering based on user behavior
|
|
360
|
+
const userOrders = await prisma.order.findMany({
|
|
361
|
+
where: { customerId: userId },
|
|
362
|
+
include: { items: true },
|
|
363
|
+
take: 10,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const purchasedProducts = userOrders.flatMap(o => o.items.map(i => i.productId));
|
|
367
|
+
|
|
368
|
+
// Find users who bought similar products
|
|
369
|
+
const similarUsers = await prisma.order.findMany({
|
|
370
|
+
where: {
|
|
371
|
+
items: {
|
|
372
|
+
some: {
|
|
373
|
+
productId: { in: purchasedProducts },
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
customerId: { not: userId },
|
|
377
|
+
},
|
|
378
|
+
select: { customerId: true },
|
|
379
|
+
distinct: ['customerId'],
|
|
380
|
+
take: 50,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Get products bought by similar users
|
|
384
|
+
const recommendations = await prisma.product.findMany({
|
|
385
|
+
where: {
|
|
386
|
+
orders: {
|
|
387
|
+
some: {
|
|
388
|
+
customerId: { in: similarUsers.map(u => u.customerId) },
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
id: { not: excludeProductId },
|
|
392
|
+
status: 'active',
|
|
393
|
+
},
|
|
394
|
+
take: 8,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
return recommendations;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private async getContentBasedRecommendations(product: Product): Promise<Product[]> {
|
|
401
|
+
// Find similar products based on categories and tags
|
|
402
|
+
return prisma.product.findMany({
|
|
403
|
+
where: {
|
|
404
|
+
OR: [
|
|
405
|
+
{ categories: { some: { id: { in: product.categories.map(c => c.id) } } } },
|
|
406
|
+
{ tags: { hasSome: product.tags } },
|
|
407
|
+
],
|
|
408
|
+
id: { not: product.id },
|
|
409
|
+
status: 'active',
|
|
410
|
+
},
|
|
411
|
+
orderBy: { salesCount: 'desc' },
|
|
412
|
+
take: 8,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Shopping Cart Service
|
|
418
|
+
class ShoppingCartService {
|
|
419
|
+
async getCart(sessionId: string): Promise<Cart> {
|
|
420
|
+
const cartKey = `cart:${sessionId}`;
|
|
421
|
+
const cartData = await redis.get(cartKey);
|
|
422
|
+
|
|
423
|
+
if (!cartData) {
|
|
424
|
+
return this.createEmptyCart(sessionId);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const cart = JSON.parse(cartData);
|
|
428
|
+
|
|
429
|
+
// Validate and update prices
|
|
430
|
+
await this.validateCartItems(cart);
|
|
431
|
+
|
|
432
|
+
return cart;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async addToCart(sessionId: string, productId: string, variantId?: string, quantity: number = 1): Promise<Cart> {
|
|
436
|
+
const cart = await this.getCart(sessionId);
|
|
437
|
+
|
|
438
|
+
// Check inventory
|
|
439
|
+
const available = await this.checkInventory(productId, variantId, quantity);
|
|
440
|
+
if (!available) {
|
|
441
|
+
throw new Error('Insufficient inventory');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Reserve inventory
|
|
445
|
+
await this.reserveInventory(productId, variantId, quantity);
|
|
446
|
+
|
|
447
|
+
// Get product details
|
|
448
|
+
const product = await prisma.product.findUnique({
|
|
449
|
+
where: { id: productId },
|
|
450
|
+
include: { variants: true },
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (!product) {
|
|
454
|
+
throw new Error('Product not found');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Determine price
|
|
458
|
+
const variant = variantId ? product.variants.find(v => v.id === variantId) : null;
|
|
459
|
+
const price = variant?.price || product.price;
|
|
460
|
+
|
|
461
|
+
// Check if item already in cart
|
|
462
|
+
const existingItem = cart.items.find(
|
|
463
|
+
item => item.productId === productId && item.variantId === variantId
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
if (existingItem) {
|
|
467
|
+
existingItem.quantity += quantity;
|
|
468
|
+
} else {
|
|
469
|
+
cart.items.push({
|
|
470
|
+
id: uuidv4(),
|
|
471
|
+
productId,
|
|
472
|
+
variantId,
|
|
473
|
+
quantity,
|
|
474
|
+
price,
|
|
475
|
+
product: {
|
|
476
|
+
name: product.name,
|
|
477
|
+
slug: product.slug,
|
|
478
|
+
image: product.images[0]?.url,
|
|
479
|
+
},
|
|
480
|
+
variant: variant ? {
|
|
481
|
+
name: variant.name,
|
|
482
|
+
attributes: variant.attributes,
|
|
483
|
+
} : undefined,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Update cart totals
|
|
488
|
+
this.calculateCartTotals(cart);
|
|
489
|
+
|
|
490
|
+
// Save cart
|
|
491
|
+
await this.saveCart(cart);
|
|
492
|
+
|
|
493
|
+
// Track event
|
|
494
|
+
await this.trackCartEvent('add_to_cart', {
|
|
495
|
+
sessionId,
|
|
496
|
+
productId,
|
|
497
|
+
variantId,
|
|
498
|
+
quantity,
|
|
499
|
+
value: price * quantity,
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
return cart;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async updateCartItem(sessionId: string, itemId: string, quantity: number): Promise<Cart> {
|
|
506
|
+
const cart = await this.getCart(sessionId);
|
|
507
|
+
const item = cart.items.find(i => i.id === itemId);
|
|
508
|
+
|
|
509
|
+
if (!item) {
|
|
510
|
+
throw new Error('Item not found in cart');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const quantityDiff = quantity - item.quantity;
|
|
514
|
+
|
|
515
|
+
if (quantityDiff > 0) {
|
|
516
|
+
// Check additional inventory
|
|
517
|
+
const available = await this.checkInventory(item.productId, item.variantId, quantityDiff);
|
|
518
|
+
if (!available) {
|
|
519
|
+
throw new Error('Insufficient inventory');
|
|
520
|
+
}
|
|
521
|
+
await this.reserveInventory(item.productId, item.variantId, quantityDiff);
|
|
522
|
+
} else if (quantityDiff < 0) {
|
|
523
|
+
// Release inventory
|
|
524
|
+
await this.releaseInventory(item.productId, item.variantId, Math.abs(quantityDiff));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (quantity === 0) {
|
|
528
|
+
// Remove item
|
|
529
|
+
cart.items = cart.items.filter(i => i.id !== itemId);
|
|
530
|
+
} else {
|
|
531
|
+
item.quantity = quantity;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
this.calculateCartTotals(cart);
|
|
535
|
+
await this.saveCart(cart);
|
|
536
|
+
|
|
537
|
+
return cart;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async applyDiscount(sessionId: string, code: string): Promise<Cart> {
|
|
541
|
+
const cart = await this.getCart(sessionId);
|
|
542
|
+
|
|
543
|
+
// Validate discount code
|
|
544
|
+
const discount = await prisma.discountCode.findUnique({
|
|
545
|
+
where: { code },
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
if (!discount || !this.isDiscountValid(discount)) {
|
|
549
|
+
throw new Error('Invalid discount code');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Check usage limits
|
|
553
|
+
if (discount.maxUses && discount.usageCount >= discount.maxUses) {
|
|
554
|
+
throw new Error('Discount code has reached its usage limit');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Apply discount
|
|
558
|
+
cart.discountCode = code;
|
|
559
|
+
cart.discount = this.calculateDiscount(cart, discount);
|
|
560
|
+
|
|
561
|
+
this.calculateCartTotals(cart);
|
|
562
|
+
await this.saveCart(cart);
|
|
563
|
+
|
|
564
|
+
return cart;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private createEmptyCart(sessionId: string): Cart {
|
|
568
|
+
return {
|
|
569
|
+
id: uuidv4(),
|
|
570
|
+
sessionId,
|
|
571
|
+
items: [],
|
|
572
|
+
subtotal: 0,
|
|
573
|
+
tax: 0,
|
|
574
|
+
shipping: 0,
|
|
575
|
+
discount: 0,
|
|
576
|
+
total: 0,
|
|
577
|
+
createdAt: new Date(),
|
|
578
|
+
updatedAt: new Date(),
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private async validateCartItems(cart: Cart) {
|
|
583
|
+
for (const item of cart.items) {
|
|
584
|
+
const product = await prisma.product.findUnique({
|
|
585
|
+
where: { id: item.productId },
|
|
586
|
+
include: { variants: true },
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
if (!product || product.status !== 'active') {
|
|
590
|
+
// Remove unavailable product
|
|
591
|
+
cart.items = cart.items.filter(i => i.id !== item.id);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Update price if changed
|
|
596
|
+
const variant = item.variantId ? product.variants.find(v => v.id === item.variantId) : null;
|
|
597
|
+
const currentPrice = variant?.price || product.price;
|
|
598
|
+
|
|
599
|
+
if (item.price !== currentPrice) {
|
|
600
|
+
item.price = currentPrice;
|
|
601
|
+
item.priceChanged = true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private calculateCartTotals(cart: Cart) {
|
|
607
|
+
cart.subtotal = cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
|
608
|
+
cart.tax = this.calculateTax(cart);
|
|
609
|
+
cart.shipping = this.calculateShipping(cart);
|
|
610
|
+
cart.total = cart.subtotal + cart.tax + cart.shipping - cart.discount;
|
|
611
|
+
cart.updatedAt = new Date();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private calculateTax(cart: Cart): number {
|
|
615
|
+
// Simplified tax calculation
|
|
616
|
+
const taxRate = 0.08; // 8% tax rate
|
|
617
|
+
return cart.subtotal * taxRate;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private calculateShipping(cart: Cart): number {
|
|
621
|
+
if (cart.subtotal >= config.shipping.freeShippingThreshold) {
|
|
622
|
+
return 0;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Calculate based on weight/dimensions
|
|
626
|
+
const baseShipping = 5.99;
|
|
627
|
+
const itemShipping = cart.items.length * 0.5;
|
|
628
|
+
|
|
629
|
+
return baseShipping + itemShipping;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private calculateDiscount(cart: Cart, discount: any): number {
|
|
633
|
+
if (discount.type === 'percentage') {
|
|
634
|
+
return cart.subtotal * (discount.value / 100);
|
|
635
|
+
} else if (discount.type === 'fixed') {
|
|
636
|
+
return Math.min(discount.value, cart.subtotal);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return 0;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private isDiscountValid(discount: any): boolean {
|
|
643
|
+
const now = new Date();
|
|
644
|
+
|
|
645
|
+
if (discount.startDate && new Date(discount.startDate) > now) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (discount.endDate && new Date(discount.endDate) < now) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return discount.active;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private async saveCart(cart: Cart) {
|
|
657
|
+
const cartKey = `cart:${cart.sessionId}`;
|
|
658
|
+
await redis.setex(cartKey, config.cart.sessionTimeout, JSON.stringify(cart));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private async checkInventory(productId: string, variantId?: string, quantity: number): Promise<boolean> {
|
|
662
|
+
const inventory = await prisma.inventory.findFirst({
|
|
663
|
+
where: {
|
|
664
|
+
productId,
|
|
665
|
+
variantId: variantId || null,
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
if (!inventory || !inventory.trackInventory) {
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const available = inventory.quantity - inventory.reserved;
|
|
674
|
+
|
|
675
|
+
if (available >= quantity) {
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return inventory.allowBackorder;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private async reserveInventory(productId: string, variantId?: string, quantity: number) {
|
|
683
|
+
await prisma.inventory.update({
|
|
684
|
+
where: {
|
|
685
|
+
productId_variantId: {
|
|
686
|
+
productId,
|
|
687
|
+
variantId: variantId || null,
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
data: {
|
|
691
|
+
reserved: { increment: quantity },
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// Set expiration for reservation
|
|
696
|
+
const reservationKey = `reservation:${productId}:${variantId || 'default'}:${Date.now()}`;
|
|
697
|
+
await redis.setex(reservationKey, config.cart.reservationTime / 1000, quantity.toString());
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private async releaseInventory(productId: string, variantId?: string, quantity: number) {
|
|
701
|
+
await prisma.inventory.update({
|
|
702
|
+
where: {
|
|
703
|
+
productId_variantId: {
|
|
704
|
+
productId,
|
|
705
|
+
variantId: variantId || null,
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
data: {
|
|
709
|
+
reserved: { decrement: quantity },
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private async trackCartEvent(event: string, data: any) {
|
|
715
|
+
// Send to analytics
|
|
716
|
+
await sqs.sendMessage({
|
|
717
|
+
QueueUrl: process.env.ANALYTICS_QUEUE_URL!,
|
|
718
|
+
MessageBody: JSON.stringify({
|
|
719
|
+
event,
|
|
720
|
+
data,
|
|
721
|
+
timestamp: new Date().toISOString(),
|
|
722
|
+
}),
|
|
723
|
+
}).promise();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Checkout Service
|
|
728
|
+
class CheckoutService {
|
|
729
|
+
private cart = new ShoppingCartService();
|
|
730
|
+
|
|
731
|
+
async createCheckout(sessionId: string, checkoutData: any): Promise<Checkout> {
|
|
732
|
+
const cart = await this.cart.getCart(sessionId);
|
|
733
|
+
|
|
734
|
+
if (cart.items.length === 0) {
|
|
735
|
+
throw new Error('Cart is empty');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Validate checkout data
|
|
739
|
+
const validated = this.validateCheckoutData(checkoutData);
|
|
740
|
+
|
|
741
|
+
// Calculate final amounts
|
|
742
|
+
const shipping = await this.calculateShipping(validated.shippingAddress, cart);
|
|
743
|
+
const tax = await this.calculateTax(validated.shippingAddress, cart);
|
|
744
|
+
|
|
745
|
+
// Create checkout session
|
|
746
|
+
const checkout = {
|
|
747
|
+
id: uuidv4(),
|
|
748
|
+
sessionId,
|
|
749
|
+
cart,
|
|
750
|
+
email: validated.email,
|
|
751
|
+
shippingAddress: validated.shippingAddress,
|
|
752
|
+
billingAddress: validated.billingAddress || validated.shippingAddress,
|
|
753
|
+
shippingMethod: validated.shippingMethod,
|
|
754
|
+
shipping,
|
|
755
|
+
tax,
|
|
756
|
+
total: cart.subtotal + shipping + tax - cart.discount,
|
|
757
|
+
status: 'pending',
|
|
758
|
+
expiresAt: new Date(Date.now() + 3600000), // 1 hour
|
|
759
|
+
createdAt: new Date(),
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
// Save checkout
|
|
763
|
+
await redis.setex(`checkout:${checkout.id}`, 3600, JSON.stringify(checkout));
|
|
764
|
+
|
|
765
|
+
return checkout;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async processPayment(checkoutId: string, paymentMethod: string, paymentDetails: any): Promise<Order> {
|
|
769
|
+
// Get checkout
|
|
770
|
+
const checkoutData = await redis.get(`checkout:${checkoutId}`);
|
|
771
|
+
if (!checkoutData) {
|
|
772
|
+
throw new Error('Checkout session expired');
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const checkout = JSON.parse(checkoutData);
|
|
776
|
+
|
|
777
|
+
// Process payment based on method
|
|
778
|
+
let paymentResult;
|
|
779
|
+
switch (paymentMethod) {
|
|
780
|
+
case 'card':
|
|
781
|
+
paymentResult = await this.processCardPayment(checkout, paymentDetails);
|
|
782
|
+
break;
|
|
783
|
+
case 'paypal':
|
|
784
|
+
paymentResult = await this.processPayPalPayment(checkout, paymentDetails);
|
|
785
|
+
break;
|
|
786
|
+
case 'klarna':
|
|
787
|
+
paymentResult = await this.processKlarnaPayment(checkout, paymentDetails);
|
|
788
|
+
break;
|
|
789
|
+
default:
|
|
790
|
+
throw new Error('Unsupported payment method');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (!paymentResult.success) {
|
|
794
|
+
throw new Error(paymentResult.error || 'Payment failed');
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Create order
|
|
798
|
+
const order = await this.createOrder(checkout, paymentResult);
|
|
799
|
+
|
|
800
|
+
// Clear cart and checkout
|
|
801
|
+
await redis.del(`cart:${checkout.sessionId}`);
|
|
802
|
+
await redis.del(`checkout:${checkoutId}`);
|
|
803
|
+
|
|
804
|
+
// Send order confirmation
|
|
805
|
+
await this.sendOrderConfirmation(order);
|
|
806
|
+
|
|
807
|
+
// Queue fulfillment
|
|
808
|
+
await this.queueFulfillment(order);
|
|
809
|
+
|
|
810
|
+
return order;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private async processCardPayment(checkout: any, paymentDetails: any) {
|
|
814
|
+
try {
|
|
815
|
+
// Create Stripe payment intent
|
|
816
|
+
const paymentIntent = await stripe.paymentIntents.create({
|
|
817
|
+
amount: Math.round(checkout.total * 100),
|
|
818
|
+
currency: 'usd',
|
|
819
|
+
payment_method: paymentDetails.paymentMethodId,
|
|
820
|
+
confirm: true,
|
|
821
|
+
metadata: {
|
|
822
|
+
checkoutId: checkout.id,
|
|
823
|
+
},
|
|
824
|
+
shipping: {
|
|
825
|
+
name: checkout.shippingAddress.name,
|
|
826
|
+
address: {
|
|
827
|
+
line1: checkout.shippingAddress.line1,
|
|
828
|
+
line2: checkout.shippingAddress.line2,
|
|
829
|
+
city: checkout.shippingAddress.city,
|
|
830
|
+
state: checkout.shippingAddress.state,
|
|
831
|
+
postal_code: checkout.shippingAddress.postalCode,
|
|
832
|
+
country: checkout.shippingAddress.country,
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
success: paymentIntent.status === 'succeeded',
|
|
839
|
+
transactionId: paymentIntent.id,
|
|
840
|
+
amount: paymentIntent.amount / 100,
|
|
841
|
+
};
|
|
842
|
+
} catch (error: any) {
|
|
843
|
+
logger.error('Card payment error:', error);
|
|
844
|
+
return {
|
|
845
|
+
success: false,
|
|
846
|
+
error: error.message,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private async processPayPalPayment(checkout: any, paymentDetails: any) {
|
|
852
|
+
// PayPal integration
|
|
853
|
+
return {
|
|
854
|
+
success: true,
|
|
855
|
+
transactionId: 'PAYPAL_' + Date.now(),
|
|
856
|
+
amount: checkout.total,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
private async processKlarnaPayment(checkout: any, paymentDetails: any) {
|
|
861
|
+
// Klarna Buy Now Pay Later integration
|
|
862
|
+
return {
|
|
863
|
+
success: true,
|
|
864
|
+
transactionId: 'KLARNA_' + Date.now(),
|
|
865
|
+
amount: checkout.total,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
private async createOrder(checkout: any, paymentResult: any): Promise<Order> {
|
|
870
|
+
const orderNumber = this.generateOrderNumber();
|
|
871
|
+
|
|
872
|
+
const order = await prisma.order.create({
|
|
873
|
+
data: {
|
|
874
|
+
orderNumber,
|
|
875
|
+
customerId: checkout.customerId,
|
|
876
|
+
email: checkout.email,
|
|
877
|
+
status: OrderStatus.PAID,
|
|
878
|
+
items: {
|
|
879
|
+
create: checkout.cart.items.map((item: any) => ({
|
|
880
|
+
productId: item.productId,
|
|
881
|
+
variantId: item.variantId,
|
|
882
|
+
quantity: item.quantity,
|
|
883
|
+
price: item.price,
|
|
884
|
+
total: item.price * item.quantity,
|
|
885
|
+
})),
|
|
886
|
+
},
|
|
887
|
+
subtotal: checkout.cart.subtotal,
|
|
888
|
+
tax: checkout.tax,
|
|
889
|
+
shipping: checkout.shipping,
|
|
890
|
+
discount: checkout.cart.discount,
|
|
891
|
+
total: checkout.total,
|
|
892
|
+
shippingAddress: checkout.shippingAddress,
|
|
893
|
+
billingAddress: checkout.billingAddress,
|
|
894
|
+
payment: {
|
|
895
|
+
method: paymentResult.method,
|
|
896
|
+
transactionId: paymentResult.transactionId,
|
|
897
|
+
amount: paymentResult.amount,
|
|
898
|
+
status: 'completed',
|
|
899
|
+
},
|
|
900
|
+
metadata: {
|
|
901
|
+
sessionId: checkout.sessionId,
|
|
902
|
+
checkoutId: checkout.id,
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
include: {
|
|
906
|
+
items: true,
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// Update inventory
|
|
911
|
+
for (const item of checkout.cart.items) {
|
|
912
|
+
await this.updateInventory(item.productId, item.variantId, item.quantity);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Update product sales counts
|
|
916
|
+
await this.updateSalesMetrics(order);
|
|
917
|
+
|
|
918
|
+
return order;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
private generateOrderNumber(): string {
|
|
922
|
+
const timestamp = Date.now().toString(36).toUpperCase();
|
|
923
|
+
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
924
|
+
return `ORD-${timestamp}-${random}`;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
private async updateInventory(productId: string, variantId: string | null, quantity: number) {
|
|
928
|
+
await prisma.inventory.update({
|
|
929
|
+
where: {
|
|
930
|
+
productId_variantId: {
|
|
931
|
+
productId,
|
|
932
|
+
variantId: variantId || null,
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
data: {
|
|
936
|
+
quantity: { decrement: quantity },
|
|
937
|
+
reserved: { decrement: quantity },
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// Check for low stock
|
|
942
|
+
const inventory = await prisma.inventory.findUnique({
|
|
943
|
+
where: {
|
|
944
|
+
productId_variantId: {
|
|
945
|
+
productId,
|
|
946
|
+
variantId: variantId || null,
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
if (inventory && inventory.quantity <= config.inventory.lowStockThreshold) {
|
|
952
|
+
await this.sendLowStockAlert(productId, variantId, inventory.quantity);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private async updateSalesMetrics(order: Order) {
|
|
957
|
+
// Update product sales counts and revenue
|
|
958
|
+
for (const item of order.items) {
|
|
959
|
+
await prisma.product.update({
|
|
960
|
+
where: { id: item.productId },
|
|
961
|
+
data: {
|
|
962
|
+
salesCount: { increment: item.quantity },
|
|
963
|
+
revenue: { increment: item.total },
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Update daily sales metrics
|
|
969
|
+
await prisma.salesMetric.upsert({
|
|
970
|
+
where: {
|
|
971
|
+
date: new Date().toISOString().split('T')[0],
|
|
972
|
+
},
|
|
973
|
+
create: {
|
|
974
|
+
date: new Date().toISOString().split('T')[0],
|
|
975
|
+
orders: 1,
|
|
976
|
+
revenue: order.total,
|
|
977
|
+
items: order.items.length,
|
|
978
|
+
},
|
|
979
|
+
update: {
|
|
980
|
+
orders: { increment: 1 },
|
|
981
|
+
revenue: { increment: order.total },
|
|
982
|
+
items: { increment: order.items.length },
|
|
983
|
+
},
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
private async sendOrderConfirmation(order: Order) {
|
|
988
|
+
// Queue email notification
|
|
989
|
+
await sqs.sendMessage({
|
|
990
|
+
QueueUrl: process.env.EMAIL_QUEUE_URL!,
|
|
991
|
+
MessageBody: JSON.stringify({
|
|
992
|
+
type: 'order_confirmation',
|
|
993
|
+
to: order.email,
|
|
994
|
+
orderId: order.id,
|
|
995
|
+
orderNumber: order.orderNumber,
|
|
996
|
+
}),
|
|
997
|
+
}).promise();
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private async queueFulfillment(order: Order) {
|
|
1001
|
+
// Send to fulfillment queue
|
|
1002
|
+
await sqs.sendMessage({
|
|
1003
|
+
QueueUrl: process.env.FULFILLMENT_QUEUE_URL!,
|
|
1004
|
+
MessageBody: JSON.stringify({
|
|
1005
|
+
orderId: order.id,
|
|
1006
|
+
orderNumber: order.orderNumber,
|
|
1007
|
+
items: order.items,
|
|
1008
|
+
shippingAddress: order.shippingAddress,
|
|
1009
|
+
shippingMethod: order.shippingMethod,
|
|
1010
|
+
}),
|
|
1011
|
+
}).promise();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
private async sendLowStockAlert(productId: string, variantId: string | null, quantity: number) {
|
|
1015
|
+
// Send alert to inventory management
|
|
1016
|
+
await sqs.sendMessage({
|
|
1017
|
+
QueueUrl: process.env.ALERTS_QUEUE_URL!,
|
|
1018
|
+
MessageBody: JSON.stringify({
|
|
1019
|
+
type: 'low_stock',
|
|
1020
|
+
productId,
|
|
1021
|
+
variantId,
|
|
1022
|
+
quantity,
|
|
1023
|
+
threshold: config.inventory.lowStockThreshold,
|
|
1024
|
+
}),
|
|
1025
|
+
}).promise();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private validateCheckoutData(data: any) {
|
|
1029
|
+
const schema = z.object({
|
|
1030
|
+
email: z.string().email(),
|
|
1031
|
+
shippingAddress: z.object({
|
|
1032
|
+
name: z.string(),
|
|
1033
|
+
line1: z.string(),
|
|
1034
|
+
line2: z.string().optional(),
|
|
1035
|
+
city: z.string(),
|
|
1036
|
+
state: z.string(),
|
|
1037
|
+
postalCode: z.string(),
|
|
1038
|
+
country: z.string(),
|
|
1039
|
+
phone: z.string().optional(),
|
|
1040
|
+
}),
|
|
1041
|
+
billingAddress: z.object({
|
|
1042
|
+
name: z.string(),
|
|
1043
|
+
line1: z.string(),
|
|
1044
|
+
line2: z.string().optional(),
|
|
1045
|
+
city: z.string(),
|
|
1046
|
+
state: z.string(),
|
|
1047
|
+
postalCode: z.string(),
|
|
1048
|
+
country: z.string(),
|
|
1049
|
+
}).optional(),
|
|
1050
|
+
shippingMethod: z.string(),
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
return schema.parse(data);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
private async calculateShipping(address: any, cart: any): Promise<number> {
|
|
1057
|
+
// Get shipping rates from carriers
|
|
1058
|
+
const rates = await this.getShippingRates(address, cart);
|
|
1059
|
+
|
|
1060
|
+
// Return selected method rate
|
|
1061
|
+
return rates[0]?.amount || 0;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
private async getShippingRates(address: any, cart: any): Promise<any[]> {
|
|
1065
|
+
// Integration with shipping carriers
|
|
1066
|
+
// This would call APIs for UPS, FedEx, USPS, etc.
|
|
1067
|
+
return [
|
|
1068
|
+
{ carrier: 'USPS', service: 'Priority', amount: 5.99, days: 3 },
|
|
1069
|
+
{ carrier: 'UPS', service: 'Ground', amount: 8.99, days: 5 },
|
|
1070
|
+
{ carrier: 'FedEx', service: '2-Day', amount: 15.99, days: 2 },
|
|
1071
|
+
];
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
private async calculateTax(address: any, cart: any): Promise<number> {
|
|
1075
|
+
if (!config.tax.enableAutomaticCalculation) {
|
|
1076
|
+
return 0;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Check if we have nexus in this state
|
|
1080
|
+
if (!config.tax.nexusStates.includes(address.state)) {
|
|
1081
|
+
return 0;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Get tax rate for location
|
|
1085
|
+
const taxRate = await this.getTaxRate(address);
|
|
1086
|
+
|
|
1087
|
+
// Calculate tax on taxable items
|
|
1088
|
+
const taxableAmount = cart.items.reduce((sum: number, item: any) => {
|
|
1089
|
+
// Check if product is taxable
|
|
1090
|
+
const isTaxable = item.product?.taxable !== false;
|
|
1091
|
+
return sum + (isTaxable ? item.price * item.quantity : 0);
|
|
1092
|
+
}, 0);
|
|
1093
|
+
|
|
1094
|
+
return taxableAmount * taxRate;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
private async getTaxRate(address: any): Promise<number> {
|
|
1098
|
+
// Integration with tax calculation service (TaxJar, Avalara, etc.)
|
|
1099
|
+
// Simplified for example
|
|
1100
|
+
const stateTaxRates: Record<string, number> = {
|
|
1101
|
+
'CA': 0.0725,
|
|
1102
|
+
'NY': 0.08,
|
|
1103
|
+
'TX': 0.0625,
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
return stateTaxRates[address.state] || 0;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Order Management Service
|
|
1111
|
+
class OrderManagementService {
|
|
1112
|
+
async getOrder(orderId: string): Promise<Order | null> {
|
|
1113
|
+
return prisma.order.findUnique({
|
|
1114
|
+
where: { id: orderId },
|
|
1115
|
+
include: {
|
|
1116
|
+
items: {
|
|
1117
|
+
include: {
|
|
1118
|
+
product: true,
|
|
1119
|
+
variant: true,
|
|
1120
|
+
},
|
|
1121
|
+
},
|
|
1122
|
+
fulfillments: true,
|
|
1123
|
+
refunds: true,
|
|
1124
|
+
},
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
async updateOrderStatus(orderId: string, status: OrderStatus, metadata?: any): Promise<Order> {
|
|
1129
|
+
const order = await prisma.order.update({
|
|
1130
|
+
where: { id: orderId },
|
|
1131
|
+
data: {
|
|
1132
|
+
status,
|
|
1133
|
+
statusHistory: {
|
|
1134
|
+
create: {
|
|
1135
|
+
status,
|
|
1136
|
+
metadata,
|
|
1137
|
+
createdAt: new Date(),
|
|
1138
|
+
},
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
include: {
|
|
1142
|
+
items: true,
|
|
1143
|
+
},
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
// Send status update notification
|
|
1147
|
+
await this.sendStatusNotification(order);
|
|
1148
|
+
|
|
1149
|
+
return order;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
async createFulfillment(orderId: string, fulfillmentData: any): Promise<Fulfillment> {
|
|
1153
|
+
const fulfillment = await prisma.fulfillment.create({
|
|
1154
|
+
data: {
|
|
1155
|
+
orderId,
|
|
1156
|
+
trackingNumber: fulfillmentData.trackingNumber,
|
|
1157
|
+
carrier: fulfillmentData.carrier,
|
|
1158
|
+
service: fulfillmentData.service,
|
|
1159
|
+
items: fulfillmentData.items,
|
|
1160
|
+
shippedAt: new Date(),
|
|
1161
|
+
estimatedDelivery: fulfillmentData.estimatedDelivery,
|
|
1162
|
+
},
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
// Update order status
|
|
1166
|
+
await this.updateOrderStatus(orderId, OrderStatus.SHIPPED, {
|
|
1167
|
+
fulfillmentId: fulfillment.id,
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
// Send shipping notification
|
|
1171
|
+
await this.sendShippingNotification(orderId, fulfillment);
|
|
1172
|
+
|
|
1173
|
+
return fulfillment;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
async processReturn(orderId: string, returnData: any): Promise<Return> {
|
|
1177
|
+
const order = await this.getOrder(orderId);
|
|
1178
|
+
if (!order) {
|
|
1179
|
+
throw new Error('Order not found');
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Validate return request
|
|
1183
|
+
if (!this.isReturnEligible(order)) {
|
|
1184
|
+
throw new Error('Order is not eligible for return');
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Create return record
|
|
1188
|
+
const returnRecord = await prisma.return.create({
|
|
1189
|
+
data: {
|
|
1190
|
+
orderId,
|
|
1191
|
+
items: returnData.items,
|
|
1192
|
+
reason: returnData.reason,
|
|
1193
|
+
status: 'pending',
|
|
1194
|
+
refundAmount: this.calculateRefundAmount(order, returnData.items),
|
|
1195
|
+
returnLabel: await this.generateReturnLabel(order),
|
|
1196
|
+
},
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// Send return instructions
|
|
1200
|
+
await this.sendReturnInstructions(order, returnRecord);
|
|
1201
|
+
|
|
1202
|
+
return returnRecord;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
async processRefund(orderId: string, amount: number, reason: string): Promise<Refund> {
|
|
1206
|
+
const order = await this.getOrder(orderId);
|
|
1207
|
+
if (!order) {
|
|
1208
|
+
throw new Error('Order not found');
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Process refund through payment gateway
|
|
1212
|
+
let refundResult;
|
|
1213
|
+
if (order.payment.method === 'card' && order.payment.transactionId) {
|
|
1214
|
+
refundResult = await stripe.refunds.create({
|
|
1215
|
+
payment_intent: order.payment.transactionId,
|
|
1216
|
+
amount: Math.round(amount * 100),
|
|
1217
|
+
reason: 'requested_by_customer',
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Create refund record
|
|
1222
|
+
const refund = await prisma.refund.create({
|
|
1223
|
+
data: {
|
|
1224
|
+
orderId,
|
|
1225
|
+
amount,
|
|
1226
|
+
reason,
|
|
1227
|
+
status: refundResult?.status || 'completed',
|
|
1228
|
+
transactionId: refundResult?.id,
|
|
1229
|
+
processedAt: new Date(),
|
|
1230
|
+
},
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
// Update order status if fully refunded
|
|
1234
|
+
const totalRefunded = await this.getTotalRefunded(orderId);
|
|
1235
|
+
if (totalRefunded >= order.total) {
|
|
1236
|
+
await this.updateOrderStatus(orderId, OrderStatus.REFUNDED);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Send refund confirmation
|
|
1240
|
+
await this.sendRefundConfirmation(order, refund);
|
|
1241
|
+
|
|
1242
|
+
return refund;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
private isReturnEligible(order: Order): boolean {
|
|
1246
|
+
// Check return policy (e.g., 30 days)
|
|
1247
|
+
const daysSinceOrder = (Date.now() - order.createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
1248
|
+
return daysSinceOrder <= 30 && order.status === OrderStatus.DELIVERED;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
private calculateRefundAmount(order: Order, returnItems: any[]): number {
|
|
1252
|
+
let refundAmount = 0;
|
|
1253
|
+
|
|
1254
|
+
for (const returnItem of returnItems) {
|
|
1255
|
+
const orderItem = order.items.find(i => i.id === returnItem.itemId);
|
|
1256
|
+
if (orderItem) {
|
|
1257
|
+
refundAmount += orderItem.price * returnItem.quantity;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Include proportional tax and shipping
|
|
1262
|
+
const refundPercentage = refundAmount / order.subtotal;
|
|
1263
|
+
refundAmount += order.tax * refundPercentage;
|
|
1264
|
+
refundAmount += order.shipping * refundPercentage;
|
|
1265
|
+
|
|
1266
|
+
return refundAmount;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
private async generateReturnLabel(order: Order): Promise<string> {
|
|
1270
|
+
// Generate return shipping label
|
|
1271
|
+
// Integration with shipping carriers
|
|
1272
|
+
return `RETURN_LABEL_${order.orderNumber}`;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
private async getTotalRefunded(orderId: string): Promise<number> {
|
|
1276
|
+
const refunds = await prisma.refund.findMany({
|
|
1277
|
+
where: { orderId },
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
return refunds.reduce((sum, refund) => sum + refund.amount, 0);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
private async sendStatusNotification(order: Order) {
|
|
1284
|
+
await sqs.sendMessage({
|
|
1285
|
+
QueueUrl: process.env.EMAIL_QUEUE_URL!,
|
|
1286
|
+
MessageBody: JSON.stringify({
|
|
1287
|
+
type: 'order_status_update',
|
|
1288
|
+
to: order.email,
|
|
1289
|
+
orderId: order.id,
|
|
1290
|
+
orderNumber: order.orderNumber,
|
|
1291
|
+
status: order.status,
|
|
1292
|
+
}),
|
|
1293
|
+
}).promise();
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
private async sendShippingNotification(orderId: string, fulfillment: Fulfillment) {
|
|
1297
|
+
const order = await this.getOrder(orderId);
|
|
1298
|
+
|
|
1299
|
+
await sqs.sendMessage({
|
|
1300
|
+
QueueUrl: process.env.EMAIL_QUEUE_URL!,
|
|
1301
|
+
MessageBody: JSON.stringify({
|
|
1302
|
+
type: 'shipping_confirmation',
|
|
1303
|
+
to: order!.email,
|
|
1304
|
+
orderId,
|
|
1305
|
+
orderNumber: order!.orderNumber,
|
|
1306
|
+
trackingNumber: fulfillment.trackingNumber,
|
|
1307
|
+
carrier: fulfillment.carrier,
|
|
1308
|
+
estimatedDelivery: fulfillment.estimatedDelivery,
|
|
1309
|
+
}),
|
|
1310
|
+
}).promise();
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
private async sendReturnInstructions(order: Order, returnRecord: Return) {
|
|
1314
|
+
await sqs.sendMessage({
|
|
1315
|
+
QueueUrl: process.env.EMAIL_QUEUE_URL!,
|
|
1316
|
+
MessageBody: JSON.stringify({
|
|
1317
|
+
type: 'return_instructions',
|
|
1318
|
+
to: order.email,
|
|
1319
|
+
orderId: order.id,
|
|
1320
|
+
returnId: returnRecord.id,
|
|
1321
|
+
returnLabel: returnRecord.returnLabel,
|
|
1322
|
+
}),
|
|
1323
|
+
}).promise();
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
private async sendRefundConfirmation(order: Order, refund: Refund) {
|
|
1327
|
+
await sqs.sendMessage({
|
|
1328
|
+
QueueUrl: process.env.EMAIL_QUEUE_URL!,
|
|
1329
|
+
MessageBody: JSON.stringify({
|
|
1330
|
+
type: 'refund_confirmation',
|
|
1331
|
+
to: order.email,
|
|
1332
|
+
orderId: order.id,
|
|
1333
|
+
orderNumber: order.orderNumber,
|
|
1334
|
+
refundAmount: refund.amount,
|
|
1335
|
+
refundReason: refund.reason,
|
|
1336
|
+
}),
|
|
1337
|
+
}).promise();
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Customer Service
|
|
1342
|
+
class CustomerService {
|
|
1343
|
+
async createAccount(data: any): Promise<Customer> {
|
|
1344
|
+
// Validate data
|
|
1345
|
+
const validated = this.validateCustomerData(data);
|
|
1346
|
+
|
|
1347
|
+
// Check if email already exists
|
|
1348
|
+
const existing = await prisma.customer.findUnique({
|
|
1349
|
+
where: { email: validated.email },
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
if (existing) {
|
|
1353
|
+
throw new Error('Email already registered');
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Hash password
|
|
1357
|
+
const hashedPassword = await bcrypt.hash(validated.password, 10);
|
|
1358
|
+
|
|
1359
|
+
// Create customer
|
|
1360
|
+
const customer = await prisma.customer.create({
|
|
1361
|
+
data: {
|
|
1362
|
+
email: validated.email,
|
|
1363
|
+
password: hashedPassword,
|
|
1364
|
+
firstName: validated.firstName,
|
|
1365
|
+
lastName: validated.lastName,
|
|
1366
|
+
phone: validated.phone,
|
|
1367
|
+
acceptsMarketing: validated.acceptsMarketing || false,
|
|
1368
|
+
verificationToken: uuidv4(),
|
|
1369
|
+
verified: false,
|
|
1370
|
+
},
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
// Send verification email
|
|
1374
|
+
await this.sendVerificationEmail(customer);
|
|
1375
|
+
|
|
1376
|
+
// Subscribe to newsletter if opted in
|
|
1377
|
+
if (customer.acceptsMarketing) {
|
|
1378
|
+
await this.subscribeToNewsletter(customer.email);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return customer;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
async login(email: string, password: string): Promise<{ customer: Customer; token: string }> {
|
|
1385
|
+
const customer = await prisma.customer.findUnique({
|
|
1386
|
+
where: { email },
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
if (!customer) {
|
|
1390
|
+
throw new Error('Invalid credentials');
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const validPassword = await bcrypt.compare(password, customer.password);
|
|
1394
|
+
if (!validPassword) {
|
|
1395
|
+
throw new Error('Invalid credentials');
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (!customer.verified) {
|
|
1399
|
+
throw new Error('Please verify your email');
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Generate JWT token
|
|
1403
|
+
const token = jwt.sign(
|
|
1404
|
+
{
|
|
1405
|
+
customerId: customer.id,
|
|
1406
|
+
email: customer.email,
|
|
1407
|
+
},
|
|
1408
|
+
process.env.JWT_SECRET!,
|
|
1409
|
+
{ expiresIn: '7d' }
|
|
1410
|
+
);
|
|
1411
|
+
|
|
1412
|
+
// Update last login
|
|
1413
|
+
await prisma.customer.update({
|
|
1414
|
+
where: { id: customer.id },
|
|
1415
|
+
data: { lastLogin: new Date() },
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
return { customer, token };
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
async addToWishlist(customerId: string, productId: string): Promise<void> {
|
|
1422
|
+
await prisma.wishlist.create({
|
|
1423
|
+
data: {
|
|
1424
|
+
customerId,
|
|
1425
|
+
productId,
|
|
1426
|
+
},
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
async getRecommendations(customerId: string): Promise<Product[]> {
|
|
1431
|
+
// Get customer's purchase history
|
|
1432
|
+
const orders = await prisma.order.findMany({
|
|
1433
|
+
where: { customerId },
|
|
1434
|
+
include: { items: true },
|
|
1435
|
+
orderBy: { createdAt: 'desc' },
|
|
1436
|
+
take: 10,
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
// Get customer's browsing history
|
|
1440
|
+
const browsingHistory = await redis.lrange(`browsing:${customerId}`, 0, 20);
|
|
1441
|
+
|
|
1442
|
+
// Get customer's wishlist
|
|
1443
|
+
const wishlist = await prisma.wishlist.findMany({
|
|
1444
|
+
where: { customerId },
|
|
1445
|
+
select: { productId: true },
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
// Generate personalized recommendations
|
|
1449
|
+
// This would use a recommendation engine (collaborative filtering, content-based, etc.)
|
|
1450
|
+
const recommendations = await this.generatePersonalizedRecommendations({
|
|
1451
|
+
orders,
|
|
1452
|
+
browsingHistory,
|
|
1453
|
+
wishlist,
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
return recommendations;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
private validateCustomerData(data: any) {
|
|
1460
|
+
const schema = z.object({
|
|
1461
|
+
email: z.string().email(),
|
|
1462
|
+
password: z.string().min(8),
|
|
1463
|
+
firstName: z.string(),
|
|
1464
|
+
lastName: z.string(),
|
|
1465
|
+
phone: z.string().optional(),
|
|
1466
|
+
acceptsMarketing: z.boolean().optional(),
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
return schema.parse(data);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private async sendVerificationEmail(customer: Customer) {
|
|
1473
|
+
await sqs.sendMessage({
|
|
1474
|
+
QueueUrl: process.env.EMAIL_QUEUE_URL!,
|
|
1475
|
+
MessageBody: JSON.stringify({
|
|
1476
|
+
type: 'email_verification',
|
|
1477
|
+
to: customer.email,
|
|
1478
|
+
customerId: customer.id,
|
|
1479
|
+
verificationToken: customer.verificationToken,
|
|
1480
|
+
}),
|
|
1481
|
+
}).promise();
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
private async subscribeToNewsletter(email: string) {
|
|
1485
|
+
// Add to mailing list (e.g., Mailchimp, SendGrid)
|
|
1486
|
+
await sqs.sendMessage({
|
|
1487
|
+
QueueUrl: process.env.MARKETING_QUEUE_URL!,
|
|
1488
|
+
MessageBody: JSON.stringify({
|
|
1489
|
+
action: 'subscribe',
|
|
1490
|
+
email,
|
|
1491
|
+
list: 'newsletter',
|
|
1492
|
+
}),
|
|
1493
|
+
}).promise();
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
private async generatePersonalizedRecommendations(data: any): Promise<Product[]> {
|
|
1497
|
+
// Simplified recommendation logic
|
|
1498
|
+
// In production, this would use ML models
|
|
1499
|
+
|
|
1500
|
+
const productIds = new Set<string>();
|
|
1501
|
+
|
|
1502
|
+
// Add products from same categories as purchased items
|
|
1503
|
+
for (const order of data.orders) {
|
|
1504
|
+
for (const item of order.items) {
|
|
1505
|
+
const product = await prisma.product.findUnique({
|
|
1506
|
+
where: { id: item.productId },
|
|
1507
|
+
include: { categories: true },
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
if (product) {
|
|
1511
|
+
const related = await prisma.product.findMany({
|
|
1512
|
+
where: {
|
|
1513
|
+
categories: {
|
|
1514
|
+
some: {
|
|
1515
|
+
id: { in: product.categories.map(c => c.id) },
|
|
1516
|
+
},
|
|
1517
|
+
},
|
|
1518
|
+
id: { not: product.id },
|
|
1519
|
+
},
|
|
1520
|
+
take: 3,
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
related.forEach(p => productIds.add(p.id));
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Add trending products
|
|
1529
|
+
const trending = await prisma.product.findMany({
|
|
1530
|
+
where: {
|
|
1531
|
+
status: 'active',
|
|
1532
|
+
id: { notIn: Array.from(productIds) },
|
|
1533
|
+
},
|
|
1534
|
+
orderBy: { salesCount: 'desc' },
|
|
1535
|
+
take: 5,
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
trending.forEach(p => productIds.add(p.id));
|
|
1539
|
+
|
|
1540
|
+
// Fetch full product details
|
|
1541
|
+
return prisma.product.findMany({
|
|
1542
|
+
where: { id: { in: Array.from(productIds) } },
|
|
1543
|
+
include: { images: true },
|
|
1544
|
+
take: 12,
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Type definitions
|
|
1550
|
+
interface Cart {
|
|
1551
|
+
id: string;
|
|
1552
|
+
sessionId: string;
|
|
1553
|
+
customerId?: string;
|
|
1554
|
+
items: CartItem[];
|
|
1555
|
+
subtotal: number;
|
|
1556
|
+
tax: number;
|
|
1557
|
+
shipping: number;
|
|
1558
|
+
discount: number;
|
|
1559
|
+
total: number;
|
|
1560
|
+
discountCode?: string;
|
|
1561
|
+
createdAt: Date;
|
|
1562
|
+
updatedAt: Date;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
interface Checkout {
|
|
1566
|
+
id: string;
|
|
1567
|
+
sessionId: string;
|
|
1568
|
+
cart: Cart;
|
|
1569
|
+
email: string;
|
|
1570
|
+
shippingAddress: Address;
|
|
1571
|
+
billingAddress: Address;
|
|
1572
|
+
shippingMethod: string;
|
|
1573
|
+
shipping: number;
|
|
1574
|
+
tax: number;
|
|
1575
|
+
total: number;
|
|
1576
|
+
status: string;
|
|
1577
|
+
expiresAt: Date;
|
|
1578
|
+
createdAt: Date;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
interface Address {
|
|
1582
|
+
name: string;
|
|
1583
|
+
line1: string;
|
|
1584
|
+
line2?: string;
|
|
1585
|
+
city: string;
|
|
1586
|
+
state: string;
|
|
1587
|
+
postalCode: string;
|
|
1588
|
+
country: string;
|
|
1589
|
+
phone?: string;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
interface OrderItem {
|
|
1593
|
+
id: string;
|
|
1594
|
+
orderId: string;
|
|
1595
|
+
productId: string;
|
|
1596
|
+
variantId?: string;
|
|
1597
|
+
quantity: number;
|
|
1598
|
+
price: number;
|
|
1599
|
+
total: number;
|
|
1600
|
+
product?: Product;
|
|
1601
|
+
variant?: ProductVariant;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
interface PaymentInfo {
|
|
1605
|
+
method: string;
|
|
1606
|
+
transactionId: string;
|
|
1607
|
+
amount: number;
|
|
1608
|
+
status: string;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
interface FulfillmentInfo {
|
|
1612
|
+
status: string;
|
|
1613
|
+
trackingNumber?: string;
|
|
1614
|
+
carrier?: string;
|
|
1615
|
+
shippedAt?: Date;
|
|
1616
|
+
deliveredAt?: Date;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
interface Fulfillment {
|
|
1620
|
+
id: string;
|
|
1621
|
+
orderId: string;
|
|
1622
|
+
trackingNumber: string;
|
|
1623
|
+
carrier: string;
|
|
1624
|
+
service: string;
|
|
1625
|
+
items: any[];
|
|
1626
|
+
shippedAt: Date;
|
|
1627
|
+
estimatedDelivery?: Date;
|
|
1628
|
+
deliveredAt?: Date;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
interface Return {
|
|
1632
|
+
id: string;
|
|
1633
|
+
orderId: string;
|
|
1634
|
+
items: any[];
|
|
1635
|
+
reason: string;
|
|
1636
|
+
status: string;
|
|
1637
|
+
refundAmount: number;
|
|
1638
|
+
returnLabel: string;
|
|
1639
|
+
receivedAt?: Date;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
interface Refund {
|
|
1643
|
+
id: string;
|
|
1644
|
+
orderId: string;
|
|
1645
|
+
amount: number;
|
|
1646
|
+
reason: string;
|
|
1647
|
+
status: string;
|
|
1648
|
+
transactionId?: string;
|
|
1649
|
+
processedAt: Date;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
interface Customer {
|
|
1653
|
+
id: string;
|
|
1654
|
+
email: string;
|
|
1655
|
+
password: string;
|
|
1656
|
+
firstName: string;
|
|
1657
|
+
lastName: string;
|
|
1658
|
+
phone?: string;
|
|
1659
|
+
acceptsMarketing: boolean;
|
|
1660
|
+
verificationToken?: string;
|
|
1661
|
+
verified: boolean;
|
|
1662
|
+
lastLogin?: Date;
|
|
1663
|
+
createdAt: Date;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
interface ProductImage {
|
|
1667
|
+
id: string;
|
|
1668
|
+
url: string;
|
|
1669
|
+
alt?: string;
|
|
1670
|
+
position: number;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
interface Category {
|
|
1674
|
+
id: string;
|
|
1675
|
+
name: string;
|
|
1676
|
+
slug: string;
|
|
1677
|
+
parentId?: string;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
interface InventoryLocation {
|
|
1681
|
+
locationId: string;
|
|
1682
|
+
quantity: number;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
interface Dimensions {
|
|
1686
|
+
length: number;
|
|
1687
|
+
width: number;
|
|
1688
|
+
height: number;
|
|
1689
|
+
unit: string;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
interface SEOData {
|
|
1693
|
+
title: string;
|
|
1694
|
+
description: string;
|
|
1695
|
+
keywords?: string[];
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// Export services
|
|
1699
|
+
export {
|
|
1700
|
+
ProductCatalogService,
|
|
1701
|
+
ShoppingCartService,
|
|
1702
|
+
CheckoutService,
|
|
1703
|
+
OrderManagementService,
|
|
1704
|
+
CustomerService,
|
|
1705
|
+
};
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
## Best Practices
|
|
1709
|
+
|
|
1710
|
+
### 1. Performance Optimization
|
|
1711
|
+
- Implement caching strategies (Redis, CDN)
|
|
1712
|
+
- Use database indexing and query optimization
|
|
1713
|
+
- Implement lazy loading and pagination
|
|
1714
|
+
- Optimize images and assets
|
|
1715
|
+
- Use async processing for heavy operations
|
|
1716
|
+
|
|
1717
|
+
### 2. Security
|
|
1718
|
+
- PCI DSS compliance for payment processing
|
|
1719
|
+
- Secure session management
|
|
1720
|
+
- Input validation and sanitization
|
|
1721
|
+
- Rate limiting and DDoS protection
|
|
1722
|
+
- Regular security audits
|
|
1723
|
+
|
|
1724
|
+
### 3. Scalability
|
|
1725
|
+
- Microservices architecture for large platforms
|
|
1726
|
+
- Message queuing for async processing
|
|
1727
|
+
- Database sharding for large catalogs
|
|
1728
|
+
- Load balancing and auto-scaling
|
|
1729
|
+
- Content delivery networks (CDN)
|
|
1730
|
+
|
|
1731
|
+
### 4. User Experience
|
|
1732
|
+
- Fast page load times (< 3 seconds)
|
|
1733
|
+
- Mobile-responsive design
|
|
1734
|
+
- Intuitive navigation and search
|
|
1735
|
+
- Guest checkout options
|
|
1736
|
+
- Multiple payment methods
|
|
1737
|
+
|
|
1738
|
+
### 5. Conversion Optimization
|
|
1739
|
+
- A/B testing for layouts and features
|
|
1740
|
+
- Abandoned cart recovery
|
|
1741
|
+
- Personalized recommendations
|
|
1742
|
+
- Social proof (reviews, ratings)
|
|
1743
|
+
- Clear return policies
|
|
1744
|
+
|
|
1745
|
+
## Common Patterns
|
|
1746
|
+
|
|
1747
|
+
1. **Shopping Cart**: Session-based or persistent cart management
|
|
1748
|
+
2. **Inventory Reservation**: Temporary stock reservation during checkout
|
|
1749
|
+
3. **Order State Machine**: Managing order lifecycle and transitions
|
|
1750
|
+
4. **Payment Gateway Integration**: Abstract payment processing
|
|
1751
|
+
5. **Webhook Handling**: Real-time updates from external services
|
|
1752
|
+
6. **Event Sourcing**: Track all changes for audit and analytics
|
|
1753
|
+
7. **CQRS**: Separate read/write models for performance
|
|
1754
|
+
8. **Saga Pattern**: Distributed transaction management
|
|
1755
|
+
|
|
1756
|
+
Remember: E-commerce requires careful attention to performance, security, and user experience. Always prioritize customer data protection and payment security while optimizing for conversions and scalability.
|