skilldrop-cli 0.16.29 → 0.16.31
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/bin/skilldrop.js +160 -33
- package/catalogue.json +10 -4
- package/model-routing.json +24 -0
- package/package.json +1 -1
- package/packs/ai-engineering/skills/mcp-pairing/SKILL.md +93 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/eval_queries.json +13 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/evals.json +76 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/2/mcp.json +32 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/.mcp.json +47 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/docs/pr-loop.md +12 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/5/docs/oncall-triage.md +18 -0
- package/packs/ai-engineering/skills/mcp-pairing/examples/build-loop-github-jira-datadog.md +69 -0
- package/packs/ai-engineering/skills/mcp-pairing/manifest.json +17 -0
- package/packs/ai-engineering/skills/mcp-pairing/reference.md +136 -0
- package/packs/ai-engineering/skills/mcp-pairing/templates/mcp-pairing.md +43 -0
- package/packs/dev-team/loops/build/LOOP.md +4 -2
- package/packs/dev-team/loops/build/loop.json +2 -2
- package/packs/dev-team/loops/release/LOOP.md +7 -5
- package/packs/dev-team/loops/release/loop.json +2 -2
- package/packs/dev-team/skills/codebase-tour/SKILL.md +115 -0
- package/packs/dev-team/skills/codebase-tour/evals/eval_queries.json +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/evals.json +77 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/.github/workflows/ci.yml +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/README.md +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/openapi.yaml +21 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/package-lock.json +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/package.json +29 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/config.ts +20 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/filter.ts +24 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/repository.ts +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/errors.ts +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/generated/api-types.ts +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/handlers/shipments.ts +38 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/routes.ts +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/jobs/export-shipments.ts +22 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/server.ts +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/domain/filter.test.ts +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/http/shipments.test.ts +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/tsconfig.json +10 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/vitest.config.ts +5 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/.github/workflows/ci.yml +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/README.md +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/noxfile.py +14 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/pyproject.toml +22 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/__init__.py +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/_version.py +4 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/cli.py +32 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/detect.py +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/errors.py +2 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/money.py +14 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/__init__.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/acme.py +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/base.py +24 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/harbor.py +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/fixtures/acme_sample.csv +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_acme.py +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_detect.py +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +28 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/Makefile +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/README.md +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/cmd/tidepool/main.go +24 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +5 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/config/config.go +38 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher.go +52 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher_test.go +42 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/sender.go +32 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/replay/replay.go +31 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/db.go +35 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql +11 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql.go +30 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/schema.sql +6 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/store_integration_test.go +21 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/sqlc.yaml +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/.env.example +4 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/.github/workflows/ci.yml +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/Makefile +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/README.md +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/data/rates.csv +4 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/pyproject.toml +19 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/scripts/gen_rates.py +29 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/__init__.py +0 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/__init__.py +0 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/preview.py +14 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/routes.py +36 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/config.py +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/discounts.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/__init__.py +0 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/rates_table.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/main.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/shipping.py +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/tax.py +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/conftest.py +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_discounts.py +11 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_tax.py +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/.env.example +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/.github/workflows/ci.yml +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/README.md +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/package.json +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/openapi.json +30 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/package.json +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/db.js +19 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/flags.js +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/generated/schema.js +32 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/middleware/validate.js +20 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/routes/bookings.js +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/server.js +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/bookings.js +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/pricing.js +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/test/pricing.test.js +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/package.json +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/bookingForm.js +11 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/format.js +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/test/format.test.js +12 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/scripts/gen-schema.mjs +27 -0
- package/packs/dev-team/skills/codebase-tour/examples/refund-window.md +158 -0
- package/packs/dev-team/skills/codebase-tour/manifest.json +47 -0
- package/packs/dev-team/skills/codebase-tour/scripts/repo_facts.py +403 -0
- package/packs/dev-team/skills/codebase-tour/templates/codebase-tour.md +85 -0
- package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +101 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/eval_queries.json +11 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +77 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/.github/workflows/ci.yml +20 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/pr.diff +87 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/quillmark-CHANGELOG.md +24 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/comment.js +6 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/note.js +8 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/test/note.test.js +12 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/ci.yml +23 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/release.yml +23 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/pr.diff +32 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/release-notes.md +20 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/.github/workflows/ci.yml +18 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/Dockerfile +8 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/PR.md +36 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/pr.diff +47 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/client.py +24 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/reader.py +32 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/tests/test_reader.py +15 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/upstream/quillstream-CHANGELOG.md +37 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/ci.yml +22 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/release.yml +25 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.nvmrc +1 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-412.md +16 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-413.md +15 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/package.json +21 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-412.diff +26 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-413.diff +84 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/scripts/release.mjs +18 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/release-notary-CHANGELOG.md +16 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/setup-toolchain-RELEASES.md +22 -0
- package/packs/dev-team/skills/dependency-upgrade-review/examples/inputs/ledgerline.diff +38 -0
- package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +91 -0
- package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +50 -0
- package/packs/dev-team/skills/dependency-upgrade-review/reference.md +99 -0
- package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +718 -0
- package/packs/dev-team/skills/refactor-plan/SKILL.md +168 -0
- package/packs/dev-team/skills/refactor-plan/evals/eval_queries.json +46 -0
- package/packs/dev-team/skills/refactor-plan/evals/evals.json +106 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/debt-register-row.md +5 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/in-flight-prs.md +7 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/order_service.py +74 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/test-coverage.txt +14 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/3/imports.txt +22 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/3/register.md +3 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/open-prs.md +6 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/tech-debt-register.md +7 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/reports/monthly_revenue.py +44 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/src/billing/invoice.py +233 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/src/partners/statement.py +29 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/tests/billing/test_payments.py +13 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/ci-notes.md +5 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/open-prs.txt +4 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/scripts/nightly-digest.js +36 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/auth/passwordReset.ts +16 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/index.ts +2 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/legacyMailer.ts +149 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/orders/onOrderShipped.ts +15 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/tests/notify/legacyMailer.test.ts +7 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/DunningJob.kt +29 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/InvoiceService.kt +157 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/pdf/InvoicePdfRenderer.kt +36 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/test/kotlin/billing/InvoiceServiceTest.kt +40 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/adr-007-persistence.md +26 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/open-prs.md +9 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/notes/open-work.md +19 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/CheckoutPanel.tsx +207 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/MiniCart.tsx +33 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/__tests__/CheckoutPanel.test.tsx +13 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/pages/OrderReview.tsx +24 -0
- package/packs/dev-team/skills/refactor-plan/examples/search-ranker.md +104 -0
- package/packs/dev-team/skills/refactor-plan/manifest.json +36 -0
- package/packs/dev-team/skills/refactor-plan/reference.md +149 -0
- package/packs/dev-team/skills/refactor-plan/templates/refactor-plan.md +82 -0
- package/packs/dev-team/skills/rollout-plan/SKILL.md +157 -0
- package/packs/dev-team/skills/rollout-plan/evals/eval_queries.json +12 -0
- package/packs/dev-team/skills/rollout-plan/evals/evals.json +109 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/2/metrics-snapshot.txt +11 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/2/pr-description.md +28 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/4/PR_DESCRIPTION.md +24 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/4/metrics_snapshot.txt +7 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/5/docs/tax-engine-v2.md +24 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/6/flags/smart_reminders_v2.yaml +14 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/6/notes/push-pipeline.md +10 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/7/docs/ratelimiter-v2.md +21 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/7/ops/traffic-summary.txt +9 -0
- package/packs/dev-team/skills/rollout-plan/examples/sync-engine-workspaces.md +102 -0
- package/packs/dev-team/skills/rollout-plan/manifest.json +35 -0
- package/packs/dev-team/skills/rollout-plan/reference.md +100 -0
- package/packs/dev-team/skills/rollout-plan/scripts/canary_math.py +261 -0
- package/packs/dev-team/skills/rollout-plan/templates/rollout-plan.md +50 -0
- package/packs/product-manager/loops/discover/LOOP.md +5 -4
- package/packs/product-manager/loops/discover/loop.json +2 -2
- package/packs/product-manager/skills/feedback-synthesis/SKILL.md +182 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/eval_queries.json +46 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/evals.json +76 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/1/interview-notes.md +47 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/1/nps-verbatims.csv +13 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/1/support-tickets.csv +36 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/3/churn-exit-survey.csv +11 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/3/sales-call-notes.md +55 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/4/csat_comments_q3.csv +17 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/4/support_tickets_q3.csv +49 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/5/app_reviews_last_60_days.csv +32 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/5/interview_notes_round1.md +48 -0
- package/packs/product-manager/skills/feedback-synthesis/examples/booking-app-feedback.md +139 -0
- package/packs/product-manager/skills/feedback-synthesis/examples/inputs/booking-app-coded.csv +17 -0
- package/packs/product-manager/skills/feedback-synthesis/manifest.json +53 -0
- package/packs/product-manager/skills/feedback-synthesis/reference.md +89 -0
- package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +355 -0
- package/packs/product-manager/skills/feedback-synthesis/templates/coding-sheet.csv +6 -0
- package/packs/product-manager/skills/feedback-synthesis/templates/synthesis.md +42 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
package billing
|
|
2
|
+
|
|
3
|
+
import jakarta.persistence.EntityManager
|
|
4
|
+
import jakarta.persistence.PersistenceContext
|
|
5
|
+
import org.springframework.stereotype.Service
|
|
6
|
+
import org.springframework.transaction.annotation.Transactional
|
|
7
|
+
import java.math.BigDecimal
|
|
8
|
+
import java.math.RoundingMode
|
|
9
|
+
import java.time.Clock
|
|
10
|
+
import java.time.LocalDate
|
|
11
|
+
|
|
12
|
+
@Service
|
|
13
|
+
class InvoiceService(
|
|
14
|
+
private val invoiceRepository: InvoiceRepository,
|
|
15
|
+
private val taxRateRepository: TaxRateRepository,
|
|
16
|
+
private val customerRepository: CustomerRepository,
|
|
17
|
+
private val clock: Clock,
|
|
18
|
+
) {
|
|
19
|
+
|
|
20
|
+
@PersistenceContext
|
|
21
|
+
private lateinit var em: EntityManager
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------
|
|
24
|
+
// Creation
|
|
25
|
+
// ---------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
@Transactional
|
|
28
|
+
fun createInvoice(customerId: Long, lines: List<LineRequest>): Invoice {
|
|
29
|
+
val customer = customerRepository.findById(customerId)
|
|
30
|
+
.orElseThrow { IllegalArgumentException("unknown customer $customerId") }
|
|
31
|
+
require(lines.isNotEmpty()) { "invoice needs at least one line" }
|
|
32
|
+
|
|
33
|
+
val invoice = Invoice(
|
|
34
|
+
customer = customer,
|
|
35
|
+
issuedOn = LocalDate.now(clock),
|
|
36
|
+
dueOn = LocalDate.now(clock).plusDays(customer.paymentTermsDays.toLong()),
|
|
37
|
+
status = InvoiceStatus.OPEN,
|
|
38
|
+
)
|
|
39
|
+
lines.forEach { req ->
|
|
40
|
+
val rate = taxRateRepository.findByRegionAndCategory(customer.region, req.category)
|
|
41
|
+
?: taxRateRepository.findDefault(customer.region)
|
|
42
|
+
invoice.lines.add(
|
|
43
|
+
InvoiceLine(
|
|
44
|
+
invoice = invoice,
|
|
45
|
+
description = req.description,
|
|
46
|
+
quantity = req.quantity,
|
|
47
|
+
unitPrice = req.unitPrice,
|
|
48
|
+
taxRate = rate.percent,
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
recalculate(invoice)
|
|
53
|
+
return invoiceRepository.save(invoice)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------
|
|
57
|
+
// Totals and tax
|
|
58
|
+
// ---------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
fun recalculate(invoice: Invoice) {
|
|
61
|
+
var net = BigDecimal.ZERO
|
|
62
|
+
var tax = BigDecimal.ZERO
|
|
63
|
+
invoice.lines.forEach { line ->
|
|
64
|
+
val lineNet = line.unitPrice.multiply(BigDecimal(line.quantity))
|
|
65
|
+
net = net.add(lineNet)
|
|
66
|
+
tax = tax.add(computeLineTax(lineNet, line.taxRate))
|
|
67
|
+
}
|
|
68
|
+
invoice.netTotal = net.setScale(2, RoundingMode.HALF_EVEN)
|
|
69
|
+
invoice.taxTotal = tax
|
|
70
|
+
invoice.grossTotal = invoice.netTotal.add(invoice.taxTotal).add(invoice.lateFeeTotal)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Tax is rounded per line (HALF_EVEN), not on the invoice total.
|
|
74
|
+
// Finance signed this off in 2023; do not change without them.
|
|
75
|
+
fun computeLineTax(lineNet: BigDecimal, percent: BigDecimal): BigDecimal =
|
|
76
|
+
lineNet.multiply(percent).divide(BigDecimal(100)).setScale(2, RoundingMode.HALF_EVEN)
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------
|
|
79
|
+
// Overdue and late fees
|
|
80
|
+
// ---------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
fun findOverdue(asOf: LocalDate): List<Invoice> =
|
|
83
|
+
em.createQuery(
|
|
84
|
+
"select i from Invoice i join fetch i.customer " +
|
|
85
|
+
"where i.status in (:open, :dunning) and i.dueOn < :asOf",
|
|
86
|
+
Invoice::class.java,
|
|
87
|
+
)
|
|
88
|
+
.setParameter("open", InvoiceStatus.OPEN)
|
|
89
|
+
.setParameter("dunning", InvoiceStatus.IN_DUNNING)
|
|
90
|
+
.setParameter("asOf", asOf)
|
|
91
|
+
.resultList
|
|
92
|
+
|
|
93
|
+
@Transactional
|
|
94
|
+
fun applyLateFees(asOf: LocalDate): Int {
|
|
95
|
+
var count = 0
|
|
96
|
+
findOverdue(asOf).forEach { invoice ->
|
|
97
|
+
val daysLate = asOf.toEpochDay() - invoice.dueOn.toEpochDay()
|
|
98
|
+
if (daysLate < invoice.customer.lateFeeGraceDays) return@forEach
|
|
99
|
+
if (invoice.customer.lateFeeExempt) return@forEach
|
|
100
|
+
|
|
101
|
+
val fee = invoice.netTotal
|
|
102
|
+
.multiply(BigDecimal("0.015"))
|
|
103
|
+
.multiply(BigDecimal(daysLate / 30 + 1))
|
|
104
|
+
.setScale(2, RoundingMode.HALF_UP)
|
|
105
|
+
if (fee > invoice.lateFeeTotal) {
|
|
106
|
+
invoice.lateFeeTotal = fee
|
|
107
|
+
recalculate(invoice)
|
|
108
|
+
em.merge(invoice)
|
|
109
|
+
count++
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return count
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------
|
|
116
|
+
// Payment
|
|
117
|
+
// ---------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
@Transactional
|
|
120
|
+
fun markPaid(invoiceId: Long, amount: BigDecimal, paidOn: LocalDate) {
|
|
121
|
+
val invoice = invoiceRepository.findById(invoiceId)
|
|
122
|
+
.orElseThrow { IllegalArgumentException("unknown invoice $invoiceId") }
|
|
123
|
+
check(invoice.status != InvoiceStatus.VOID) { "cannot pay a void invoice" }
|
|
124
|
+
|
|
125
|
+
invoice.amountPaid = invoice.amountPaid.add(amount)
|
|
126
|
+
if (invoice.amountPaid >= invoice.grossTotal) {
|
|
127
|
+
invoice.status = InvoiceStatus.PAID
|
|
128
|
+
invoice.paidOn = paidOn
|
|
129
|
+
}
|
|
130
|
+
// native update keeps the customer balance in step; the ORM mapping
|
|
131
|
+
// for Customer.balance is read-only (see V31__customer_balance.sql)
|
|
132
|
+
em.createNativeQuery(
|
|
133
|
+
"update customer set balance = balance - :amount where id = :id"
|
|
134
|
+
)
|
|
135
|
+
.setParameter("amount", amount)
|
|
136
|
+
.setParameter("id", invoice.customer.id)
|
|
137
|
+
.executeUpdate()
|
|
138
|
+
invoiceRepository.save(invoice)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@Transactional
|
|
142
|
+
fun void(invoiceId: Long, reason: String) {
|
|
143
|
+
val invoice = invoiceRepository.findById(invoiceId)
|
|
144
|
+
.orElseThrow { IllegalArgumentException("unknown invoice $invoiceId") }
|
|
145
|
+
check(invoice.amountPaid.signum() == 0) { "cannot void a part-paid invoice" }
|
|
146
|
+
invoice.status = InvoiceStatus.VOID
|
|
147
|
+
invoice.voidReason = reason
|
|
148
|
+
invoiceRepository.save(invoice)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
data class LineRequest(
|
|
153
|
+
val description: String,
|
|
154
|
+
val quantity: Int,
|
|
155
|
+
val unitPrice: BigDecimal,
|
|
156
|
+
val category: String,
|
|
157
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package billing.pdf
|
|
2
|
+
|
|
3
|
+
import billing.Invoice
|
|
4
|
+
import org.springframework.stereotype.Component
|
|
5
|
+
import java.math.BigDecimal
|
|
6
|
+
import java.math.RoundingMode
|
|
7
|
+
|
|
8
|
+
@Component
|
|
9
|
+
class InvoicePdfRenderer(private val templates: PdfTemplates) {
|
|
10
|
+
|
|
11
|
+
fun render(invoice: Invoice): ByteArray {
|
|
12
|
+
val rows = invoice.lines.map { line ->
|
|
13
|
+
val lineNet = line.unitPrice.multiply(BigDecimal(line.quantity))
|
|
14
|
+
PdfRow(
|
|
15
|
+
description = line.description,
|
|
16
|
+
quantity = line.quantity,
|
|
17
|
+
unitPrice = line.unitPrice,
|
|
18
|
+
net = lineNet,
|
|
19
|
+
tax = taxFor(lineNet, line.taxRate),
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
return templates.invoice(
|
|
23
|
+
number = invoice.number,
|
|
24
|
+
customerName = invoice.customer.name,
|
|
25
|
+
rows = rows,
|
|
26
|
+
net = invoice.netTotal,
|
|
27
|
+
tax = rows.fold(BigDecimal.ZERO) { acc, r -> acc.add(r.tax) },
|
|
28
|
+
lateFees = invoice.lateFeeTotal,
|
|
29
|
+
gross = invoice.grossTotal,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// copied from InvoiceService.computeLineTax so the renderer has no service dependency
|
|
34
|
+
private fun taxFor(lineNet: BigDecimal, percent: BigDecimal): BigDecimal =
|
|
35
|
+
lineNet.multiply(percent).divide(BigDecimal(100)).setScale(2, RoundingMode.HALF_UP)
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package billing
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.jupiter.api.Test
|
|
5
|
+
import org.springframework.beans.factory.annotation.Autowired
|
|
6
|
+
import org.springframework.boot.test.context.SpringBootTest
|
|
7
|
+
import org.springframework.transaction.annotation.Transactional
|
|
8
|
+
import java.math.BigDecimal
|
|
9
|
+
|
|
10
|
+
@SpringBootTest
|
|
11
|
+
@Transactional
|
|
12
|
+
class InvoiceServiceTest @Autowired constructor(
|
|
13
|
+
private val invoiceService: InvoiceService,
|
|
14
|
+
private val fixtures: TestFixtures,
|
|
15
|
+
) {
|
|
16
|
+
|
|
17
|
+
@Test
|
|
18
|
+
fun `creates an open invoice with totals`() {
|
|
19
|
+
val customer = fixtures.customer(region = "DE", paymentTermsDays = 30)
|
|
20
|
+
fixtures.taxRate(region = "DE", category = "service", percent = "19")
|
|
21
|
+
|
|
22
|
+
val invoice = invoiceService.createInvoice(
|
|
23
|
+
customer.id,
|
|
24
|
+
listOf(LineRequest("Consulting", 2, BigDecimal("100.00"), "service")),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
assertThat(invoice.status).isEqualTo(InvoiceStatus.OPEN)
|
|
28
|
+
assertThat(invoice.netTotal).isEqualByComparingTo("200.00")
|
|
29
|
+
assertThat(invoice.taxTotal).isEqualByComparingTo("38.00")
|
|
30
|
+
assertThat(invoice.grossTotal).isEqualByComparingTo("238.00")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test
|
|
34
|
+
fun `rejects an invoice with no lines`() {
|
|
35
|
+
val customer = fixtures.customer(region = "DE", paymentTermsDays = 30)
|
|
36
|
+
org.junit.jupiter.api.assertThrows<IllegalArgumentException> {
|
|
37
|
+
invoiceService.createInvoice(customer.id, emptyList())
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# ADR-007: Move billing-service persistence from Hibernate to jOOQ
|
|
2
|
+
|
|
3
|
+
Status: Accepted (2026-09-14)
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Hibernate has bitten us three times this year: lazy-loading surprises in the
|
|
8
|
+
PDF renderer, a dirty-checking flush that wrote a half-updated invoice during an
|
|
9
|
+
exception, and slow startup from entity scanning. Most of our queries are
|
|
10
|
+
already hand-written JPQL or native SQL.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
Replace Hibernate/Spring Data JPA with jOOQ in billing-service during Q1 2027.
|
|
15
|
+
|
|
16
|
+
## Precondition
|
|
17
|
+
|
|
18
|
+
The ORM swap must be a persistence-only change. Before it starts, invoice
|
|
19
|
+
business rules (totals, tax, late fees, payment state transitions) must live in
|
|
20
|
+
code that does not import `jakarta.persistence` or depend on `EntityManager`,
|
|
21
|
+
and must be testable without a database.
|
|
22
|
+
|
|
23
|
+
## Out of scope
|
|
24
|
+
|
|
25
|
+
- Changing tax or late-fee rules.
|
|
26
|
+
- The reporting database (separate service).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Open PRs touching billing-service (as of 2026-10-19)
|
|
2
|
+
|
|
3
|
+
| PR | Title | Author | Files | State |
|
|
4
|
+
|------|-----------------------------------------|---------|----------------------------------------------------|-------------------------------|
|
|
5
|
+
| #412 | Cap late fees at 10% of net for EU | priya.n | InvoiceService.kt (applyLateFees), InvoiceServiceTest.kt | Approved, waiting on finance sign-off, expected to merge this week |
|
|
6
|
+
| #415 | Credit notes | tomasz | InvoiceService.kt (new issueCreditNote, changes void), CreditNote.kt, V44__credit_notes.sql | Draft, ~3 weeks out |
|
|
7
|
+
| #409 | Bump Spring Boot 3.3.4 -> 3.3.5 | bot | build.gradle.kts | Green, ready |
|
|
8
|
+
|
|
9
|
+
Feature work continues on billing-service through Q4; no freeze is planned.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# web: open work (updated Mon 19 Oct 2026)
|
|
2
|
+
|
|
3
|
+
## Release
|
|
4
|
+
- **v4.8 ships Tue 3 Nov.** Code freeze Fri 30 Oct, 17:00. After freeze only
|
|
5
|
+
release-blocker fixes go to main.
|
|
6
|
+
|
|
7
|
+
## Open PRs
|
|
8
|
+
- **#231 Gift cards at checkout** (dana) - adds gift card entry + balance
|
|
9
|
+
deduction to `CheckoutPanel.tsx` (new state, changes the totals block and
|
|
10
|
+
`submit()`). In v4.8 scope. Review this week, expected merge by Wed 28 Oct.
|
|
11
|
+
- **#236 Address autocomplete** (lee) - swaps the address inputs in
|
|
12
|
+
`CheckoutPanel.tsx` for a lookup widget. NOT in v4.8; parked until after release.
|
|
13
|
+
- **#238 Bump vite 5.4 -> 5.5** (bot) - unrelated.
|
|
14
|
+
|
|
15
|
+
## Tickets
|
|
16
|
+
- **WEB-512** Checkout total in header cart sometimes 1p different from the
|
|
17
|
+
checkout page. Reported twice by support. Unassigned.
|
|
18
|
+
- **WEB-530** Second payment provider (pay-by-bank) in Q1 2027. Needs the
|
|
19
|
+
payment step to stop assuming card fields.
|
package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/CheckoutPanel.tsx
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { api } from "../lib/api";
|
|
3
|
+
import { CartItem, Address, ShippingOption } from "../types";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
items: CartItem[];
|
|
7
|
+
onComplete: (orderId: string) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function CheckoutPanel({ items, onComplete }: Props) {
|
|
11
|
+
const [step, setStep] = useState<"address" | "shipping" | "payment">("address");
|
|
12
|
+
const [email, setEmail] = useState("");
|
|
13
|
+
const [address, setAddress] = useState<Address>({
|
|
14
|
+
line1: "", line2: "", city: "", postcode: "", country: "GB",
|
|
15
|
+
});
|
|
16
|
+
const [addressErrors, setAddressErrors] = useState<Record<string, string>>({});
|
|
17
|
+
const [shippingOptions, setShippingOptions] = useState<ShippingOption[]>([]);
|
|
18
|
+
const [shipping, setShipping] = useState<ShippingOption | null>(null);
|
|
19
|
+
const [promoCode, setPromoCode] = useState("");
|
|
20
|
+
const [promoPercent, setPromoPercent] = useState(0);
|
|
21
|
+
const [promoError, setPromoError] = useState("");
|
|
22
|
+
const [cardNumber, setCardNumber] = useState("");
|
|
23
|
+
const [cardExpiry, setCardExpiry] = useState("");
|
|
24
|
+
const [cardCvc, setCardCvc] = useState("");
|
|
25
|
+
const [submitting, setSubmitting] = useState(false);
|
|
26
|
+
const [submitError, setSubmitError] = useState("");
|
|
27
|
+
|
|
28
|
+
// ---- totals (all money in pence) ----------------------------------
|
|
29
|
+
const subtotal = items.reduce((sum, i) => sum + i.unitPence * i.qty, 0);
|
|
30
|
+
const discount = Math.round(subtotal * (promoPercent / 100));
|
|
31
|
+
const afterDiscount = subtotal - discount;
|
|
32
|
+
const freeShipping = afterDiscount > 5000;
|
|
33
|
+
const shippingPence = freeShipping ? 0 : shipping ? shipping.pence : 0;
|
|
34
|
+
const total = afterDiscount + shippingPence;
|
|
35
|
+
|
|
36
|
+
// ---- draft persistence ---------------------------------------------
|
|
37
|
+
// keeps the form if the user navigates away
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
localStorage.setItem(
|
|
40
|
+
"checkout_draft",
|
|
41
|
+
JSON.stringify({ email, address, promoCode, total })
|
|
42
|
+
);
|
|
43
|
+
}, [email, address, promoCode, total]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const raw = localStorage.getItem("checkout_draft");
|
|
47
|
+
if (raw) {
|
|
48
|
+
const d = JSON.parse(raw);
|
|
49
|
+
setEmail(d.email || "");
|
|
50
|
+
if (d.address) setAddress(d.address);
|
|
51
|
+
if (d.promoCode) setPromoCode(d.promoCode);
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
// ---- address --------------------------------------------------------
|
|
56
|
+
function validateAddress(): boolean {
|
|
57
|
+
const errs: Record<string, string> = {};
|
|
58
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) errs.email = "Enter a valid email";
|
|
59
|
+
if (!address.line1.trim()) errs.line1 = "Required";
|
|
60
|
+
if (!address.city.trim()) errs.city = "Required";
|
|
61
|
+
if (address.country === "GB" && !/^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i.test(address.postcode)) {
|
|
62
|
+
errs.postcode = "Enter a valid UK postcode";
|
|
63
|
+
}
|
|
64
|
+
if (address.country !== "GB" && !address.postcode.trim()) errs.postcode = "Required";
|
|
65
|
+
setAddressErrors(errs);
|
|
66
|
+
return Object.keys(errs).length === 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function goToShipping() {
|
|
70
|
+
if (!validateAddress()) return;
|
|
71
|
+
const res = await api.get(`/shipping/options?country=${address.country}&postcode=${address.postcode}`);
|
|
72
|
+
setShippingOptions(res.data);
|
|
73
|
+
setShipping(res.data[0] ?? null);
|
|
74
|
+
setStep("shipping");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- promo ----------------------------------------------------------
|
|
78
|
+
async function applyPromo() {
|
|
79
|
+
setPromoError("");
|
|
80
|
+
if (!promoCode.trim()) return;
|
|
81
|
+
try {
|
|
82
|
+
const res = await api.get(`/promos/${encodeURIComponent(promoCode.trim().toUpperCase())}`);
|
|
83
|
+
if (res.data.expired) {
|
|
84
|
+
setPromoError("That code has expired");
|
|
85
|
+
setPromoPercent(0);
|
|
86
|
+
} else {
|
|
87
|
+
setPromoPercent(res.data.percent);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
setPromoError("Code not recognised");
|
|
91
|
+
setPromoPercent(0);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- payment --------------------------------------------------------
|
|
96
|
+
function validateCard(): string | null {
|
|
97
|
+
const digits = cardNumber.replace(/\s/g, "");
|
|
98
|
+
if (!/^\d{13,19}$/.test(digits)) return "Card number looks wrong";
|
|
99
|
+
let sum = 0;
|
|
100
|
+
for (let i = 0; i < digits.length; i++) {
|
|
101
|
+
let d = parseInt(digits[digits.length - 1 - i], 10);
|
|
102
|
+
if (i % 2 === 1) { d *= 2; if (d > 9) d -= 9; }
|
|
103
|
+
sum += d;
|
|
104
|
+
}
|
|
105
|
+
if (sum % 10 !== 0) return "Card number looks wrong";
|
|
106
|
+
if (!/^(0[1-9]|1[0-2])\/\d{2}$/.test(cardExpiry)) return "Expiry should be MM/YY";
|
|
107
|
+
if (!/^\d{3,4}$/.test(cardCvc)) return "CVC should be 3 or 4 digits";
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function submit() {
|
|
112
|
+
const cardErr = validateCard();
|
|
113
|
+
if (cardErr) { setSubmitError(cardErr); return; }
|
|
114
|
+
setSubmitting(true);
|
|
115
|
+
setSubmitError("");
|
|
116
|
+
try {
|
|
117
|
+
const tokenRes = await api.post("/payments/tokenise", {
|
|
118
|
+
number: cardNumber.replace(/\s/g, ""), expiry: cardExpiry, cvc: cardCvc,
|
|
119
|
+
});
|
|
120
|
+
const orderRes = await api.post("/orders", {
|
|
121
|
+
email, address, shippingId: shipping?.id, promoCode: promoPercent ? promoCode : null,
|
|
122
|
+
items: items.map((i) => ({ sku: i.sku, qty: i.qty })),
|
|
123
|
+
expectedTotalPence: total,
|
|
124
|
+
paymentToken: tokenRes.data.token,
|
|
125
|
+
});
|
|
126
|
+
onComplete(orderRes.data.id);
|
|
127
|
+
} catch (e: any) {
|
|
128
|
+
setSubmitError(e?.response?.data?.message || "Payment failed, please try again");
|
|
129
|
+
} finally {
|
|
130
|
+
setSubmitting(false);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const fmt = (p: number) => `£${(p / 100).toFixed(2)}`;
|
|
135
|
+
|
|
136
|
+
// ---- render ---------------------------------------------------------
|
|
137
|
+
return (
|
|
138
|
+
<div className="checkout">
|
|
139
|
+
<aside className="checkout__summary">
|
|
140
|
+
{items.map((i) => (
|
|
141
|
+
<div key={i.sku} className="row">
|
|
142
|
+
<span>{i.name} x {i.qty}</span>
|
|
143
|
+
<span>{fmt(i.unitPence * i.qty)}</span>
|
|
144
|
+
</div>
|
|
145
|
+
))}
|
|
146
|
+
{discount > 0 && <div className="row"><span>Discount</span><span>-{fmt(discount)}</span></div>}
|
|
147
|
+
<div className="row"><span>Shipping</span><span>{freeShipping ? "Free" : fmt(shippingPence)}</span></div>
|
|
148
|
+
<div className="row row--total"><span>Total</span><span>{fmt(total)}</span></div>
|
|
149
|
+
<div className="promo">
|
|
150
|
+
<input value={promoCode} onChange={(e) => setPromoCode(e.target.value)} placeholder="Promo code" />
|
|
151
|
+
<button onClick={applyPromo}>Apply</button>
|
|
152
|
+
{promoError && <p className="error">{promoError}</p>}
|
|
153
|
+
</div>
|
|
154
|
+
</aside>
|
|
155
|
+
|
|
156
|
+
{step === "address" && (
|
|
157
|
+
<section>
|
|
158
|
+
<h2>Where should we send it?</h2>
|
|
159
|
+
<label>Email<input value={email} onChange={(e) => setEmail(e.target.value)} /></label>
|
|
160
|
+
{addressErrors.email && <p className="error">{addressErrors.email}</p>}
|
|
161
|
+
<label>Address line 1<input value={address.line1} onChange={(e) => setAddress({ ...address, line1: e.target.value })} /></label>
|
|
162
|
+
{addressErrors.line1 && <p className="error">{addressErrors.line1}</p>}
|
|
163
|
+
<label>Address line 2<input value={address.line2} onChange={(e) => setAddress({ ...address, line2: e.target.value })} /></label>
|
|
164
|
+
<label>City<input value={address.city} onChange={(e) => setAddress({ ...address, city: e.target.value })} /></label>
|
|
165
|
+
{addressErrors.city && <p className="error">{addressErrors.city}</p>}
|
|
166
|
+
<label>Postcode<input value={address.postcode} onChange={(e) => setAddress({ ...address, postcode: e.target.value })} /></label>
|
|
167
|
+
{addressErrors.postcode && <p className="error">{addressErrors.postcode}</p>}
|
|
168
|
+
<label>Country
|
|
169
|
+
<select value={address.country} onChange={(e) => setAddress({ ...address, country: e.target.value })}>
|
|
170
|
+
<option value="GB">United Kingdom</option>
|
|
171
|
+
<option value="IE">Ireland</option>
|
|
172
|
+
<option value="FR">France</option>
|
|
173
|
+
<option value="DE">Germany</option>
|
|
174
|
+
</select>
|
|
175
|
+
</label>
|
|
176
|
+
<button onClick={goToShipping}>Continue to shipping</button>
|
|
177
|
+
</section>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{step === "shipping" && (
|
|
181
|
+
<section>
|
|
182
|
+
<h2>Delivery</h2>
|
|
183
|
+
{shippingOptions.map((o) => (
|
|
184
|
+
<label key={o.id} className="radio">
|
|
185
|
+
<input type="radio" checked={shipping?.id === o.id} onChange={() => setShipping(o)} />
|
|
186
|
+
{o.label} - {freeShipping ? "Free" : fmt(o.pence)}
|
|
187
|
+
</label>
|
|
188
|
+
))}
|
|
189
|
+
<button onClick={() => setStep("address")}>Back</button>
|
|
190
|
+
<button onClick={() => setStep("payment")} disabled={!shipping}>Continue to payment</button>
|
|
191
|
+
</section>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{step === "payment" && (
|
|
195
|
+
<section>
|
|
196
|
+
<h2>Payment</h2>
|
|
197
|
+
<label>Card number<input value={cardNumber} onChange={(e) => setCardNumber(e.target.value)} inputMode="numeric" /></label>
|
|
198
|
+
<label>Expiry (MM/YY)<input value={cardExpiry} onChange={(e) => setCardExpiry(e.target.value)} /></label>
|
|
199
|
+
<label>CVC<input value={cardCvc} onChange={(e) => setCardCvc(e.target.value)} inputMode="numeric" /></label>
|
|
200
|
+
{submitError && <p className="error">{submitError}</p>}
|
|
201
|
+
<button onClick={() => setStep("shipping")}>Back</button>
|
|
202
|
+
<button onClick={submit} disabled={submitting}>{submitting ? "Paying..." : `Pay ${fmt(total)}`}</button>
|
|
203
|
+
</section>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { CartItem } from "../types";
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
items: CartItem[];
|
|
6
|
+
promoPercent: number;
|
|
7
|
+
shippingPence: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// header dropdown cart; same maths as CheckoutPanel
|
|
11
|
+
export default function MiniCart({ items, promoPercent, shippingPence }: Props) {
|
|
12
|
+
const afterDiscount = items.reduce(
|
|
13
|
+
(sum, i) => sum + Math.round(i.unitPence * i.qty * (1 - promoPercent / 100)),
|
|
14
|
+
0
|
|
15
|
+
);
|
|
16
|
+
const discount = items.reduce((s, i) => s + i.unitPence * i.qty, 0) - afterDiscount;
|
|
17
|
+
const freeShipping = afterDiscount >= 5000;
|
|
18
|
+
const total = afterDiscount + (freeShipping ? 0 : shippingPence);
|
|
19
|
+
const fmt = (p: number) => `£${(p / 100).toFixed(2)}`;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="minicart">
|
|
23
|
+
{items.map((i) => (
|
|
24
|
+
<div key={i.sku} className="minicart__row">
|
|
25
|
+
{i.name} x {i.qty} <span>{fmt(i.unitPence * i.qty)}</span>
|
|
26
|
+
</div>
|
|
27
|
+
))}
|
|
28
|
+
{discount > 0 && <div className="minicart__row">Discount -{fmt(discount)}</div>}
|
|
29
|
+
<div className="minicart__total">Total {fmt(total)}</div>
|
|
30
|
+
{freeShipping && <p className="minicart__hint">You've got free delivery</p>}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import CheckoutPanel from "../CheckoutPanel";
|
|
4
|
+
|
|
5
|
+
const items = [
|
|
6
|
+
{ sku: "MUG-01", name: "Mug", unitPence: 1299, qty: 2 },
|
|
7
|
+
{ sku: "TEA-04", name: "Loose tea", unitPence: 850, qty: 1 },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
it("renders", () => {
|
|
11
|
+
const { container } = render(<CheckoutPanel items={items} onComplete={() => {}} />);
|
|
12
|
+
expect(container).toMatchSnapshot();
|
|
13
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
// Shown when a returning visitor lands on /checkout/review from the
|
|
5
|
+
// "you left something behind" email.
|
|
6
|
+
export default function OrderReview() {
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
const raw = localStorage.getItem("checkout_draft");
|
|
9
|
+
const draft = raw ? JSON.parse(raw) : null;
|
|
10
|
+
|
|
11
|
+
if (!draft || !draft.email) {
|
|
12
|
+
navigate("/cart");
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<main className="review">
|
|
18
|
+
<h1>Pick up where you left off</h1>
|
|
19
|
+
<p>We saved your details for {draft.email}.</p>
|
|
20
|
+
<p>Your total was <strong>£{(draft.total / 100).toFixed(2)}</strong>.</p>
|
|
21
|
+
<button onClick={() => navigate("/checkout")}>Continue to checkout</button>
|
|
22
|
+
</main>
|
|
23
|
+
);
|
|
24
|
+
}
|