skilldrop-cli 0.16.28 → 0.16.30
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 +7 -3
- package/model-routing.json +16 -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/data-analytics/loops/measure/LOOP.md +118 -0
- package/packs/data-analytics/loops/measure/evals/eval_queries.json +9 -0
- package/packs/data-analytics/loops/measure/evals/evals.json +28 -0
- package/packs/data-analytics/loops/measure/loop.json +41 -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/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/grc/loops/comply/LOOP.md +133 -0
- package/packs/grc/loops/comply/evals/eval_queries.json +9 -0
- package/packs/grc/loops/comply/evals/evals.json +30 -0
- package/packs/grc/loops/comply/loop.json +54 -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
- package/packs/research/loops/research/LOOP.md +123 -0
- package/packs/research/loops/research/evals/eval_queries.json +9 -0
- package/packs/research/loops/research/evals/evals.json +39 -0
- package/packs/research/loops/research/loop.json +35 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# shipcalc
|
|
2
|
+
|
|
3
|
+
Shipping quote service for the storefront. Given a cart it returns the shipping
|
|
4
|
+
cost, loyalty discount and VAT.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
pip install -r requirements.txt
|
|
9
|
+
|
|
10
|
+
## Run locally
|
|
11
|
+
|
|
12
|
+
export FLASK_APP=shipcalc
|
|
13
|
+
flask run
|
|
14
|
+
|
|
15
|
+
## Tests
|
|
16
|
+
|
|
17
|
+
python -m unittest discover
|
|
18
|
+
|
|
19
|
+
## Layout
|
|
20
|
+
|
|
21
|
+
- `src/shipcalc/shipping.py` - shipping cost and free-shipping rules
|
|
22
|
+
- `src/shipcalc/discounts.py` - loyalty discounts
|
|
23
|
+
- `src/shipcalc/tax.py` - VAT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shipcalc"
|
|
7
|
+
version = "2.3.0"
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = ["fastapi>=0.110", "uvicorn>=0.29"]
|
|
10
|
+
|
|
11
|
+
[project.optional-dependencies]
|
|
12
|
+
dev = ["pytest>=8", "httpx>=0.27"]
|
|
13
|
+
|
|
14
|
+
[tool.setuptools.packages.find]
|
|
15
|
+
where = ["src"]
|
|
16
|
+
|
|
17
|
+
[tool.pytest.ini_options]
|
|
18
|
+
testpaths = ["tests"]
|
|
19
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Generate src/shipcalc/generated/rates_table.py from data/rates.csv.
|
|
2
|
+
|
|
3
|
+
Usage: python scripts/gen_rates.py data/rates.csv src/shipcalc/generated/rates_table.py
|
|
4
|
+
"""
|
|
5
|
+
import csv
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
HEADER = """# DO NOT EDIT. This file is generated by scripts/gen_rates.py from data/rates.csv.
|
|
9
|
+
# Edit the CSV and run `make gen` instead. CI fails if this file is out of date.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(src: str, dest: str) -> None:
|
|
14
|
+
with open(src, newline="") as fh:
|
|
15
|
+
rows = list(csv.DictReader(fh))
|
|
16
|
+
lines = [HEADER, "RATES = {"]
|
|
17
|
+
for row in rows:
|
|
18
|
+
lines.append(
|
|
19
|
+
f' "{row["region"]}": {{"currency": "{row["currency"]}", '
|
|
20
|
+
f'"base_rate": {float(row["base_rate"])}, "per_kg": {float(row["per_kg"])}, '
|
|
21
|
+
f'"free_threshold": {float(row["free_threshold"])}}},'
|
|
22
|
+
)
|
|
23
|
+
lines.append("}")
|
|
24
|
+
with open(dest, "w") as fh:
|
|
25
|
+
fh.write("\n".join(lines) + "\n")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
main(sys.argv[1], sys.argv[2])
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Copy shown in the cart drawer ("Spend X more for free shipping")."""
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
# Kept here so the banner doesn't need the rates table at import time.
|
|
5
|
+
FREE_SHIPPING_MIN = {"EU": Decimal("50.00"), "UK": Decimal("40.00"), "US": Decimal("75.00")}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def free_shipping_banner(region: str, subtotal: Decimal) -> str:
|
|
9
|
+
threshold = FREE_SHIPPING_MIN.get(region)
|
|
10
|
+
if threshold is None:
|
|
11
|
+
return ""
|
|
12
|
+
if subtotal >= threshold:
|
|
13
|
+
return "You qualify for free shipping!"
|
|
14
|
+
return f"Spend {threshold - subtotal} more for free shipping."
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from shipcalc.api.preview import free_shipping_banner
|
|
7
|
+
from shipcalc.discounts import loyalty_discount
|
|
8
|
+
from shipcalc.shipping import shipping_cost
|
|
9
|
+
from shipcalc.tax import vat
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QuoteRequest(BaseModel):
|
|
15
|
+
region: str
|
|
16
|
+
subtotal: Decimal
|
|
17
|
+
weight_kg: Decimal
|
|
18
|
+
tier: str = "bronze"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.post("/quote")
|
|
22
|
+
def quote(req: QuoteRequest) -> dict:
|
|
23
|
+
discount = loyalty_discount(req.tier, req.subtotal)
|
|
24
|
+
shipping = shipping_cost(req.region, req.subtotal, req.weight_kg)
|
|
25
|
+
tax = vat(req.region, req.subtotal - discount + shipping)
|
|
26
|
+
return {
|
|
27
|
+
"discount": str(discount),
|
|
28
|
+
"shipping": str(shipping),
|
|
29
|
+
"tax": str(tax),
|
|
30
|
+
"total": str(req.subtotal - discount + shipping + tax),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.get("/preview/banner")
|
|
35
|
+
def banner(region: str, subtotal: Decimal) -> dict:
|
|
36
|
+
return {"banner": free_shipping_banner(region, subtotal)}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class Settings:
|
|
7
|
+
api_key: str
|
|
8
|
+
log_level: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_settings() -> Settings:
|
|
12
|
+
api_key = os.environ.get("SHIPCALC_API_KEY")
|
|
13
|
+
if not api_key:
|
|
14
|
+
raise RuntimeError("SHIPCALC_API_KEY must be set (see .env.example)")
|
|
15
|
+
return Settings(api_key=api_key, log_level=os.environ.get("SHIPCALC_LOG_LEVEL", "info"))
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
LOYALTY_RATES = {"bronze": Decimal("0.00"), "silver": Decimal("0.03"), "gold": Decimal("0.05")}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def loyalty_discount(tier: str, subtotal: Decimal) -> Decimal:
|
|
7
|
+
rate = LOYALTY_RATES.get(tier, Decimal("0.00"))
|
|
8
|
+
return (subtotal * rate).quantize(Decimal("0.01"))
|
|
File without changes
|
package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/rates_table.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# DO NOT EDIT. This file is generated by scripts/gen_rates.py from data/rates.csv.
|
|
2
|
+
# Edit the CSV and run `make gen` instead. CI fails if this file is out of date.
|
|
3
|
+
|
|
4
|
+
RATES = {
|
|
5
|
+
"EU": {"currency": "EUR", "base_rate": 4.9, "per_kg": 1.2, "free_threshold": 50.0},
|
|
6
|
+
"UK": {"currency": "GBP", "base_rate": 3.95, "per_kg": 1.1, "free_threshold": 40.0},
|
|
7
|
+
"US": {"currency": "USD", "base_rate": 5.5, "per_kg": 0.95, "free_threshold": 75.0},
|
|
8
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Shipping cost for a cart. Rates come from the generated table."""
|
|
2
|
+
import os
|
|
3
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
4
|
+
|
|
5
|
+
from shipcalc.generated.rates_table import RATES
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _free_shipping_enabled() -> bool:
|
|
9
|
+
# Rolled out per environment; off unless explicitly turned on.
|
|
10
|
+
return os.environ.get("SHIPCALC_FF_FREE_SHIPPING", "off") == "on"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def shipping_cost(region: str, subtotal: Decimal, weight_kg: Decimal) -> Decimal:
|
|
14
|
+
rate = RATES[region]
|
|
15
|
+
if _free_shipping_enabled() and subtotal >= Decimal(str(rate["free_threshold"])):
|
|
16
|
+
return Decimal("0.00")
|
|
17
|
+
cost = Decimal(str(rate["base_rate"])) + Decimal(str(rate["per_kg"])) * weight_kg
|
|
18
|
+
return cost.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from shipcalc.discounts import loyalty_discount
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_gold_gets_five_percent():
|
|
7
|
+
assert loyalty_discount("gold", Decimal("100.00")) == Decimal("5.00")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_unknown_tier_gets_nothing():
|
|
11
|
+
assert loyalty_discount("platinum", Decimal("100.00")) == Decimal("0.00")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from shipcalc.tax import vat
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.parametrize(
|
|
9
|
+
"region,taxable,expected",
|
|
10
|
+
[("EU", Decimal("100.00"), Decimal("21.00")), ("UK", Decimal("100.00"), Decimal("20.00")), ("US", Decimal("100.00"), Decimal("0.00"))],
|
|
11
|
+
)
|
|
12
|
+
def test_vat_by_region(region, taxable, expected):
|
|
13
|
+
assert vat(region, taxable) == expected
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
jobs:
|
|
4
|
+
test:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
steps:
|
|
7
|
+
- uses: actions/checkout@v4
|
|
8
|
+
- uses: actions/setup-node@v4
|
|
9
|
+
with:
|
|
10
|
+
node-version: 20
|
|
11
|
+
- run: npm install --no-audit --no-fund
|
|
12
|
+
- run: npm run gen:check
|
|
13
|
+
- run: npm test
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# cabinbook
|
|
2
|
+
|
|
3
|
+
Holiday-cabin booking: the booking API and the web booking form.
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
yarn install
|
|
8
|
+
cd api && yarn start
|
|
9
|
+
|
|
10
|
+
The API listens on http://localhost:3000.
|
|
11
|
+
|
|
12
|
+
## Running tests
|
|
13
|
+
|
|
14
|
+
yarn test
|
|
15
|
+
|
|
16
|
+
## Max stay
|
|
17
|
+
|
|
18
|
+
Bookings are limited to 14 nights; the limit lives in `api/config.js`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cabinbook",
|
|
3
|
+
"private": true,
|
|
4
|
+
"workspaces": [
|
|
5
|
+
"packages/*"
|
|
6
|
+
],
|
|
7
|
+
"scripts": {
|
|
8
|
+
"gen": "node scripts/gen-schema.mjs",
|
|
9
|
+
"gen:check": "node scripts/gen-schema.mjs --check",
|
|
10
|
+
"test": "npm test --workspaces",
|
|
11
|
+
"dev:api": "npm run dev -w @cabinbook/api"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.3",
|
|
3
|
+
"info": { "title": "cabinbook", "version": "1.4.0" },
|
|
4
|
+
"paths": {
|
|
5
|
+
"/bookings": {
|
|
6
|
+
"post": {
|
|
7
|
+
"requestBody": {
|
|
8
|
+
"content": {
|
|
9
|
+
"application/json": { "schema": { "$ref": "#/components/schemas/BookingRequest" } }
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"responses": { "201": { "description": "Created" } }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"components": {
|
|
17
|
+
"schemas": {
|
|
18
|
+
"BookingRequest": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"required": ["cabinId", "checkIn", "nights"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"cabinId": { "type": "string" },
|
|
23
|
+
"checkIn": { "type": "string", "format": "date" },
|
|
24
|
+
"nights": { "type": "integer", "minimum": 1, "maximum": 14 },
|
|
25
|
+
"guests": { "type": "integer", "minimum": 1, "maximum": 8 }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cabinbook/api",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/server.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "node --env-file=../../.env --watch src/server.js",
|
|
9
|
+
"start": "node src/server.js",
|
|
10
|
+
"test": "node --test"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"express": "^4.19.2"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// In production this is a pg pool built from DATABASE_URL; locally an in-memory stand-in.
|
|
2
|
+
const url = process.env.DATABASE_URL;
|
|
3
|
+
if (!url) {
|
|
4
|
+
throw new Error('DATABASE_URL is not set. Copy .env.example to .env.');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const bookings = [];
|
|
8
|
+
|
|
9
|
+
export const db = {
|
|
10
|
+
url,
|
|
11
|
+
async insertBooking(row) {
|
|
12
|
+
const saved = { id: String(bookings.length + 1), ...row };
|
|
13
|
+
bookings.push(saved);
|
|
14
|
+
return saved;
|
|
15
|
+
},
|
|
16
|
+
async bookingsForCabin(cabinId) {
|
|
17
|
+
return bookings.filter((b) => b.cabinId === cabinId);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Feature flags come from a comma-separated env var, e.g. FEATURE_FLAGS=long-stays,new-pricing
|
|
2
|
+
const enabled = new Set(
|
|
3
|
+
(process.env.FEATURE_FLAGS ?? '').split(',').map((f) => f.trim()).filter(Boolean),
|
|
4
|
+
);
|
|
5
|
+
|
|
6
|
+
export function isEnabled(flag) {
|
|
7
|
+
return enabled.has(flag);
|
|
8
|
+
}
|
package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/generated/schema.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// DO NOT EDIT. Generated by scripts/gen-schema.mjs from packages/api/openapi.json.
|
|
2
|
+
// Change openapi.json and run `npm run gen`.
|
|
3
|
+
|
|
4
|
+
export const schemas = {
|
|
5
|
+
"BookingRequest": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"cabinId",
|
|
9
|
+
"checkIn",
|
|
10
|
+
"nights"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"cabinId": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"checkIn": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"format": "date"
|
|
19
|
+
},
|
|
20
|
+
"nights": {
|
|
21
|
+
"type": "integer",
|
|
22
|
+
"minimum": 1,
|
|
23
|
+
"maximum": 14
|
|
24
|
+
},
|
|
25
|
+
"guests": {
|
|
26
|
+
"type": "integer",
|
|
27
|
+
"minimum": 1,
|
|
28
|
+
"maximum": 8
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/middleware/validate.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { schemas } from '../generated/schema.js';
|
|
2
|
+
|
|
3
|
+
export function validate(schemaName) {
|
|
4
|
+
const schema = schemas[schemaName];
|
|
5
|
+
return (req, res, next) => {
|
|
6
|
+
const errors = [];
|
|
7
|
+
for (const field of schema.required ?? []) {
|
|
8
|
+
if (req.body?.[field] === undefined) errors.push(`${field} is required`);
|
|
9
|
+
}
|
|
10
|
+
for (const [field, rule] of Object.entries(schema.properties)) {
|
|
11
|
+
const value = req.body?.[field];
|
|
12
|
+
if (value === undefined) continue;
|
|
13
|
+
if (rule.type === 'integer' && !Number.isInteger(value)) errors.push(`${field} must be an integer`);
|
|
14
|
+
if (rule.minimum !== undefined && value < rule.minimum) errors.push(`${field} must be >= ${rule.minimum}`);
|
|
15
|
+
if (rule.maximum !== undefined && value > rule.maximum) errors.push(`${field} must be <= ${rule.maximum}`);
|
|
16
|
+
}
|
|
17
|
+
if (errors.length) return res.status(400).json({ errors });
|
|
18
|
+
next();
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { validate } from '../middleware/validate.js';
|
|
3
|
+
import { createBooking } from '../services/bookings.js';
|
|
4
|
+
|
|
5
|
+
export const bookingsRouter = Router();
|
|
6
|
+
|
|
7
|
+
bookingsRouter.post('/bookings', validate('BookingRequest'), async (req, res, next) => {
|
|
8
|
+
try {
|
|
9
|
+
const booking = await createBooking(req.body, req.member);
|
|
10
|
+
res.status(201).json(booking);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
if (err.status) return res.status(err.status).json({ error: err.message });
|
|
13
|
+
next(err);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { bookingsRouter } from './routes/bookings.js';
|
|
3
|
+
|
|
4
|
+
const app = express();
|
|
5
|
+
app.use(express.json());
|
|
6
|
+
app.use((req, _res, next) => {
|
|
7
|
+
// Members are identified by the upstream proxy.
|
|
8
|
+
const id = req.get('x-member-id');
|
|
9
|
+
req.member = id ? { id } : null;
|
|
10
|
+
next();
|
|
11
|
+
});
|
|
12
|
+
app.use(bookingsRouter);
|
|
13
|
+
|
|
14
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
15
|
+
app.listen(port, () => console.log(`cabinbook api on :${port}`));
|
package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/bookings.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { db } from '../db.js';
|
|
2
|
+
import { isEnabled } from '../flags.js';
|
|
3
|
+
import { quote } from './pricing.js';
|
|
4
|
+
|
|
5
|
+
// Business rule: no stay may be longer than two weeks.
|
|
6
|
+
const MAX_NIGHTS = 14;
|
|
7
|
+
const LONG_STAY_MAX_NIGHTS = 28;
|
|
8
|
+
|
|
9
|
+
export function maxNightsFor(member) {
|
|
10
|
+
if (member && isEnabled('long-stays')) return LONG_STAY_MAX_NIGHTS;
|
|
11
|
+
return MAX_NIGHTS;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function createBooking({ cabinId, checkIn, nights, guests = 1 }, member) {
|
|
15
|
+
const max = maxNightsFor(member);
|
|
16
|
+
if (nights > max) {
|
|
17
|
+
const err = new Error(`Stays are limited to ${max} nights`);
|
|
18
|
+
err.status = 422;
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
const price = quote({ nights, guests });
|
|
22
|
+
return db.insertBooking({ cabinId, checkIn, nights, guests, price, memberId: member?.id ?? null });
|
|
23
|
+
}
|
package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/pricing.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const NIGHTLY_RATE = 120;
|
|
2
|
+
const EXTRA_GUEST_FEE = 15;
|
|
3
|
+
const WEEKLY_DISCOUNT = 0.1;
|
|
4
|
+
|
|
5
|
+
export function quote({ nights, guests = 1 }) {
|
|
6
|
+
const base = nights * NIGHTLY_RATE + Math.max(0, guests - 2) * EXTRA_GUEST_FEE * nights;
|
|
7
|
+
const discount = nights >= 7 ? base * WEEKLY_DISCOUNT : 0;
|
|
8
|
+
return Math.round((base - discount) * 100) / 100;
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { quote } from '../src/services/pricing.js';
|
|
4
|
+
|
|
5
|
+
test('charges the nightly rate for short stays', () => {
|
|
6
|
+
assert.equal(quote({ nights: 3 }), 360);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('adds a fee per extra guest per night', () => {
|
|
10
|
+
assert.equal(quote({ nights: 2, guests: 4 }), 300);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('applies the weekly discount from 7 nights', () => {
|
|
14
|
+
assert.equal(quote({ nights: 7 }), 756);
|
|
15
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Client-side checks for the booking form, run before POST /bookings.
|
|
2
|
+
export const MAX_NIGHTS = 14;
|
|
3
|
+
export const MAX_GUESTS = 8;
|
|
4
|
+
|
|
5
|
+
export function validateBookingForm({ nights, guests }) {
|
|
6
|
+
const errors = {};
|
|
7
|
+
if (!Number.isInteger(nights) || nights < 1) errors.nights = 'Choose at least one night';
|
|
8
|
+
else if (nights > MAX_NIGHTS) errors.nights = `Stays are limited to ${MAX_NIGHTS} nights`;
|
|
9
|
+
if (!Number.isInteger(guests) || guests < 1 || guests > MAX_GUESTS) errors.guests = `1 to ${MAX_GUESTS} guests`;
|
|
10
|
+
return errors;
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { formatNights, formatPrice } from '../src/format.js';
|
|
4
|
+
|
|
5
|
+
test('formats prices in euros with two decimals', () => {
|
|
6
|
+
assert.equal(formatPrice(756), '€756.00');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('pluralises nights', () => {
|
|
10
|
+
assert.equal(formatNights(1), '1 night');
|
|
11
|
+
assert.equal(formatNights(14), '14 nights');
|
|
12
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Generates packages/api/src/generated/schema.js from packages/api/openapi.json.
|
|
2
|
+
// Usage (from the repo root): node scripts/gen-schema.mjs [--check]
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const SRC = 'packages/api/openapi.json';
|
|
6
|
+
const DEST = 'packages/api/src/generated/schema.js';
|
|
7
|
+
|
|
8
|
+
const spec = JSON.parse(readFileSync(SRC, 'utf8'));
|
|
9
|
+
const schemas = spec.components.schemas;
|
|
10
|
+
|
|
11
|
+
const out = [
|
|
12
|
+
'// DO NOT EDIT. Generated by scripts/gen-schema.mjs from packages/api/openapi.json.',
|
|
13
|
+
'// Change openapi.json and run `npm run gen`.',
|
|
14
|
+
'',
|
|
15
|
+
`export const schemas = ${JSON.stringify(schemas, null, 2)};`,
|
|
16
|
+
'',
|
|
17
|
+
].join('\n');
|
|
18
|
+
|
|
19
|
+
if (process.argv.includes('--check')) {
|
|
20
|
+
const current = readFileSync(DEST, 'utf8');
|
|
21
|
+
if (current !== out) {
|
|
22
|
+
console.error(`${DEST} is out of date. Run \`npm run gen\`.`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
writeFileSync(DEST, out);
|
|
27
|
+
}
|