@cachly-dev/mcp-server 0.10.55 → 0.10.57
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/.github/workflows/eas-build-android.yml +58 -0
- package/.github/workflows/eas-update.yml +55 -0
- package/.github/workflows/publish.yml +2 -2
- package/BENCH.md +91 -0
- package/CHANGELOG.md +44 -0
- package/README.md +6 -34
- package/STRATEGY.md +228 -0
- package/apps/travel-chaos-organizer/.env.example +49 -0
- package/apps/travel-chaos-organizer/DEPLOY.md +328 -0
- package/apps/travel-chaos-organizer/Makefile +57 -0
- package/apps/travel-chaos-organizer/README.md +159 -0
- package/apps/travel-chaos-organizer/backend/.env.example +10 -0
- package/apps/travel-chaos-organizer/backend/Dockerfile +25 -0
- package/apps/travel-chaos-organizer/backend/alembic/env.py +56 -0
- package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic.ini +39 -0
- package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +81 -0
- package/apps/travel-chaos-organizer/backend/app/config.py +66 -0
- package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/db/database.py +20 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +71 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +31 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +4 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +5 -0
- package/apps/travel-chaos-organizer/backend/app/limiter.py +4 -0
- package/apps/travel-chaos-organizer/backend/app/logging_config.py +25 -0
- package/apps/travel-chaos-organizer/backend/app/main.py +77 -0
- package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/models/schemas.py +142 -0
- package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/routers/admin.py +237 -0
- package/apps/travel-chaos-organizer/backend/app/routers/cache.py +13 -0
- package/apps/travel-chaos-organizer/backend/app/routers/events.py +29 -0
- package/apps/travel-chaos-organizer/backend/app/routers/export.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/items.py +130 -0
- package/apps/travel-chaos-organizer/backend/app/routers/mail.py +108 -0
- package/apps/travel-chaos-organizer/backend/app/routers/parse.py +303 -0
- package/apps/travel-chaos-organizer/backend/app/routers/payments.py +179 -0
- package/apps/travel-chaos-organizer/backend/app/routers/trips.py +134 -0
- package/apps/travel-chaos-organizer/backend/app/routers/users.py +169 -0
- package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +37 -0
- package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/services/cache.py +131 -0
- package/apps/travel-chaos-organizer/backend/app/services/drip.py +128 -0
- package/apps/travel-chaos-organizer/backend/app/services/email.py +61 -0
- package/apps/travel-chaos-organizer/backend/app/services/errors.py +44 -0
- package/apps/travel-chaos-organizer/backend/app/services/notifier.py +10 -0
- package/apps/travel-chaos-organizer/backend/app/services/ollama.py +101 -0
- package/apps/travel-chaos-organizer/backend/app/services/parser.py +22 -0
- package/apps/travel-chaos-organizer/backend/app/services/quota.py +85 -0
- package/apps/travel-chaos-organizer/backend/app/services/sentry.py +11 -0
- package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +35 -0
- package/apps/travel-chaos-organizer/backend/pytest.ini +4 -0
- package/apps/travel-chaos-organizer/backend/requirements-dev.txt +6 -0
- package/apps/travel-chaos-organizer/backend/requirements.txt +25 -0
- package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/tests/conftest.py +223 -0
- package/apps/travel-chaos-organizer/backend/tests/test_admin.py +99 -0
- package/apps/travel-chaos-organizer/backend/tests/test_cache.py +142 -0
- package/apps/travel-chaos-organizer/backend/tests/test_drip.py +32 -0
- package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +121 -0
- package/apps/travel-chaos-organizer/backend/tests/test_export.py +48 -0
- package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +107 -0
- package/apps/travel-chaos-organizer/backend/tests/test_items.py +138 -0
- package/apps/travel-chaos-organizer/backend/tests/test_mail.py +113 -0
- package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +145 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +98 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parser.py +72 -0
- package/apps/travel-chaos-organizer/backend/tests/test_payments.py +163 -0
- package/apps/travel-chaos-organizer/backend/tests/test_quota.py +46 -0
- package/apps/travel-chaos-organizer/backend/tests/test_search.py +67 -0
- package/apps/travel-chaos-organizer/backend/tests/test_trips.py +123 -0
- package/apps/travel-chaos-organizer/backend/tests/test_users.py +52 -0
- package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/caddy/Caddyfile +27 -0
- package/apps/travel-chaos-organizer/docker-compose.node1.yml +31 -0
- package/apps/travel-chaos-organizer/docker-compose.override.yml +14 -0
- package/apps/travel-chaos-organizer/docker-compose.yml +128 -0
- package/apps/travel-chaos-organizer/frontend/.env.example +13 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +166 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +381 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +346 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +363 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +34 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +35 -0
- package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +91 -0
- package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +71 -0
- package/apps/travel-chaos-organizer/frontend/app/auth.tsx +29 -0
- package/apps/travel-chaos-organizer/frontend/app/index.tsx +16 -0
- package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +119 -0
- package/apps/travel-chaos-organizer/frontend/app.config.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +66 -0
- package/apps/travel-chaos-organizer/frontend/assets/icon.svg +79 -0
- package/apps/travel-chaos-organizer/frontend/assets/splash.svg +15 -0
- package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +102 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +107 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +109 -0
- package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +45 -0
- package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +19 -0
- package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +155 -0
- package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +55 -0
- package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +121 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +73 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +60 -0
- package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +42 -0
- package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +146 -0
- package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +44 -0
- package/apps/travel-chaos-organizer/frontend/eas.json +22 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +24 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +60 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/api.ts +223 -0
- package/apps/travel-chaos-organizer/frontend/lib/auth.ts +93 -0
- package/apps/travel-chaos-organizer/frontend/lib/db.ts +86 -0
- package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +13 -0
- package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +145 -0
- package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +43 -0
- package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +28 -0
- package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +44 -0
- package/apps/travel-chaos-organizer/frontend/package-lock.json +13221 -0
- package/apps/travel-chaos-organizer/frontend/package.json +44 -0
- package/apps/travel-chaos-organizer/frontend/tsconfig.json +23 -0
- package/apps/travel-chaos-organizer/keycloak/realm-export.json +59 -0
- package/apps/travel-chaos-organizer/landing/index.html +773 -0
- package/apps/travel-chaos-organizer/scripts/backup.sh +14 -0
- package/apps/travel-chaos-organizer/scripts/configure-env.sh +82 -0
- package/apps/travel-chaos-organizer/scripts/deploy.sh +54 -0
- package/apps/travel-chaos-organizer/scripts/health-check.sh +36 -0
- package/apps/travel-chaos-organizer/scripts/migrate.sh +7 -0
- package/apps/travel-chaos-organizer/scripts/server-setup.sh +85 -0
- package/apps/travel-chaos-organizer/scripts/smoke-test.sh +29 -0
- package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +71 -0
- package/apps/travel-chaos-organizer/telegram-bot/.env.example +3 -0
- package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +6 -0
- package/apps/travel-chaos-organizer/telegram-bot/bot.py +504 -0
- package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +3 -0
- package/dist/packages/telegram-notify/client.d.ts +19 -0
- package/dist/packages/telegram-notify/client.js +75 -0
- package/dist/packages/telegram-notify/client.js.map +1 -0
- package/dist/src/__tests__/auth.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/brain-flow.test.js +141 -0
- package/dist/src/__tests__/brain-flow.test.js.map +1 -0
- package/dist/src/__tests__/confidence.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-auth.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-health.test.js.map +1 -0
- package/dist/src/__tests__/e2e/cli.test.js.map +1 -0
- package/dist/src/__tests__/e2e/config.js.map +1 -0
- package/dist/src/__tests__/e2e/funnel.test.js.map +1 -0
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +1 -0
- package/dist/src/__tests__/e2e/provisioning.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/handlers-unit.test.js +0 -2
- package/dist/src/__tests__/handlers-unit.test.js.map +1 -0
- package/dist/src/__tests__/namespace.test.js.map +1 -0
- package/dist/src/__tests__/rerank.test.js +115 -0
- package/dist/src/__tests__/rerank.test.js.map +1 -0
- package/dist/src/auth.js.map +1 -0
- package/dist/src/bench/cachly-bench.d.ts +35 -0
- package/dist/src/bench/cachly-bench.js +182 -0
- package/dist/src/bench/cachly-bench.js.map +1 -0
- package/dist/src/bench/fixtures.d.ts +37 -0
- package/dist/src/bench/fixtures.js +225 -0
- package/dist/src/bench/fixtures.js.map +1 -0
- package/dist/src/ckg.js.map +1 -0
- package/dist/src/confidence.js.map +1 -0
- package/dist/src/embeddings.js.map +1 -0
- package/dist/src/handlers/advanced.js.map +1 -0
- package/dist/{handlers → src/handlers}/brain.js +335 -168
- package/dist/src/handlers/brain.js.map +1 -0
- package/dist/src/handlers/cache.js.map +1 -0
- package/dist/{handlers → src/handlers}/context.js +9 -4
- package/dist/src/handlers/context.js.map +1 -0
- package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
- package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
- package/dist/src/handlers/fedbrain.js.map +1 -0
- package/dist/src/handlers/instances.js.map +1 -0
- package/dist/{handlers → src/handlers}/roadmap.js +7 -3
- package/dist/src/handlers/roadmap.js.map +1 -0
- package/dist/src/handlers/syndicate.js.map +1 -0
- package/dist/src/handlers/tco.d.ts +16 -0
- package/dist/src/handlers/tco.js +303 -0
- package/dist/src/handlers/tco.js.map +1 -0
- package/dist/{handlers → src/handlers}/team.js +57 -7
- package/dist/src/handlers/team.js.map +1 -0
- package/dist/{index.js → src/index.js} +209 -420
- package/dist/src/index.js.map +1 -0
- package/dist/src/namespace.js.map +1 -0
- package/dist/src/notifier.d.ts +5 -0
- package/dist/src/notifier.js +6 -0
- package/dist/src/notifier.js.map +1 -0
- package/dist/src/rerank.d.ts +55 -0
- package/dist/src/rerank.js +111 -0
- package/dist/src/rerank.js.map +1 -0
- package/dist/src/search.js.map +1 -0
- package/dist/src/search.test.d.ts +1 -0
- package/dist/src/search.test.js.map +1 -0
- package/dist/{tools.d.ts → src/tools.d.ts} +226 -0
- package/dist/{tools.js → src/tools.js} +196 -0
- package/dist/src/tools.js.map +1 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +8 -7
- package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
- package/packages/telegram-notify/client.ts +88 -0
- package/packages/telegram-notify/emojis.json +22 -0
- package/packages/telegram-notify/pyproject.toml +17 -0
- package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
- package/packages/telegram-notify/telegram_notify/client.py +87 -0
- package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
- package/scripts/generate-icons.js +28 -0
- package/server.json +3 -3
- package/smithery.yaml +21 -8
- package/src/__tests__/brain-flow.test.ts +174 -0
- package/src/__tests__/handlers-unit.test.ts +1 -2
- package/src/__tests__/rerank.test.ts +130 -0
- package/src/bench/cachly-bench.ts +217 -0
- package/src/bench/fixtures.ts +256 -0
- package/src/handlers/brain.ts +345 -172
- package/src/handlers/context.ts +9 -4
- package/src/handlers/fedbrain.ts +21 -7
- package/src/handlers/roadmap.ts +7 -3
- package/src/handlers/tco.ts +347 -0
- package/src/handlers/team.ts +67 -7
- package/src/index.ts +224 -406
- package/src/notifier.ts +5 -0
- package/src/rerank.ts +148 -0
- package/src/tools.ts +210 -0
- package/tsconfig.json +2 -3
- package/dist/__tests__/auth.test.js.map +0 -1
- package/dist/__tests__/brain-flow.test.js.map +0 -1
- package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
- package/dist/__tests__/brain-predict-failures.test.js +0 -339
- package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
- package/dist/__tests__/confidence.test.js.map +0 -1
- package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/__tests__/e2e/config.js.map +0 -1
- package/dist/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/__tests__/namespace.test.js.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/ckg.js.map +0 -1
- package/dist/confidence.js.map +0 -1
- package/dist/embeddings.js.map +0 -1
- package/dist/handlers/advanced.js.map +0 -1
- package/dist/handlers/brain.js.map +0 -1
- package/dist/handlers/cache.js.map +0 -1
- package/dist/handlers/context.js.map +0 -1
- package/dist/handlers/fedbrain.js.map +0 -1
- package/dist/handlers/instances.js.map +0 -1
- package/dist/handlers/roadmap.js.map +0 -1
- package/dist/handlers/syndicate.js.map +0 -1
- package/dist/handlers/team.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/namespace.js.map +0 -1
- package/dist/search.js.map +0 -1
- package/dist/search.test.js.map +0 -1
- package/dist/tools.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/__tests__/brain-predict-failures.test.ts +0 -430
- /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
- /package/dist/{search.test.d.ts → src/__tests__/rerank.test.d.ts} +0 -0
- /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
- /package/dist/{auth.js → src/auth.js} +0 -0
- /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
- /package/dist/{ckg.js → src/ckg.js} +0 -0
- /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
- /package/dist/{confidence.js → src/confidence.js} +0 -0
- /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
- /package/dist/{embeddings.js → src/embeddings.js} +0 -0
- /package/dist/{handlers → src/handlers}/advanced.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/advanced.js +0 -0
- /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.js +0 -0
- /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.js +0 -0
- /package/dist/{handlers → src/handlers}/roadmap.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
- /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
- /package/dist/{namespace.js → src/namespace.js} +0 -0
- /package/dist/{search.d.ts → src/search.d.ts} +0 -0
- /package/dist/{search.js → src/search.js} +0 -0
- /package/dist/{search.test.js → src/search.test.js} +0 -0
- /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
- /package/dist/{utils.js → src/utils.js} +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Actionable error helpers — mirrors cachly's handleApiError pattern.
|
|
3
|
+
All user-facing messages include a concrete next step.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from fastapi import HTTPException, status
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def ollama_unavailable(url: str) -> HTTPException:
|
|
10
|
+
return HTTPException(
|
|
11
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
12
|
+
detail=(
|
|
13
|
+
f"Ollama nicht erreichbar unter {url}. "
|
|
14
|
+
"Stelle sicher, dass Ollama läuft und OLLAMA_URL korrekt gesetzt ist."
|
|
15
|
+
),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ollama_model_not_found(model: str) -> HTTPException:
|
|
20
|
+
return HTTPException(
|
|
21
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
22
|
+
detail=(
|
|
23
|
+
f"Ollama-Modell '{model}' nicht gefunden. "
|
|
24
|
+
f"Führe 'ollama pull {model}' auf deinem Server aus."
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def unsupported_file_type(mime: str) -> HTTPException:
|
|
30
|
+
return HTTPException(
|
|
31
|
+
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
|
32
|
+
detail=(
|
|
33
|
+
f"Dateityp '{mime}' wird nicht unterstützt. "
|
|
34
|
+
"Unterstützt: application/pdf, image/*, text/*"
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def low_confidence_warning(confidence: float) -> dict:
|
|
40
|
+
return {
|
|
41
|
+
"warning": f"Niedrige Erkennungssicherheit ({confidence:.0%}). "
|
|
42
|
+
"Bitte parsed_data manuell prüfen.",
|
|
43
|
+
"confidence": confidence,
|
|
44
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Re-exports from the shared telegram-notify package.
|
|
3
|
+
Canonical implementation: packages/telegram-notify/telegram_notify/client.py
|
|
4
|
+
|
|
5
|
+
Install locally: pip install -e ../../../../packages/telegram-notify
|
|
6
|
+
Docker build: COPY packages/telegram-notify /tmp/telegram-notify && pip install /tmp/telegram-notify
|
|
7
|
+
"""
|
|
8
|
+
from telegram_notify import notify, notify_user, format_message
|
|
9
|
+
|
|
10
|
+
__all__ = ["notify", "notify_user", "format_message"]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import httpx
|
|
5
|
+
from app.config import get_settings
|
|
6
|
+
from app.services.errors import ollama_unavailable, ollama_model_not_found
|
|
7
|
+
from app.services import cache as cache_svc
|
|
8
|
+
|
|
9
|
+
settings = get_settings()
|
|
10
|
+
|
|
11
|
+
PARSE_PROMPT = """You are a travel document parser. Extract structured travel information from the following content.
|
|
12
|
+
|
|
13
|
+
Return ONLY valid JSON matching this exact schema:
|
|
14
|
+
{
|
|
15
|
+
"type": "flight|train|bus|hotel|rental_car|activity|transfer|document|other",
|
|
16
|
+
"title": "short human-readable title",
|
|
17
|
+
"booking_ref": "booking/confirmation number or null",
|
|
18
|
+
"provider": "airline/hotel/company name or null",
|
|
19
|
+
"event_at": "ISO 8601 datetime or null",
|
|
20
|
+
"event_end_at": "ISO 8601 datetime or null",
|
|
21
|
+
"origin": "departure city/airport or null",
|
|
22
|
+
"destination": "arrival city/hotel address or null",
|
|
23
|
+
"passengers": ["name1", "name2"] or null,
|
|
24
|
+
"confirmation_number": "separate confirmation if different from booking_ref or null",
|
|
25
|
+
"price": "total price with currency or null",
|
|
26
|
+
"raw_summary": "one sentence summary of this travel item",
|
|
27
|
+
"confidence": 0.0 to 1.0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Content to parse:
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def parse_text(raw_text: str) -> tuple[dict, bool]:
|
|
35
|
+
"""Returns (parsed_dict, was_cached). Cachly cache hit skips Ollama entirely."""
|
|
36
|
+
cached = await cache_svc.cache_get(raw_text)
|
|
37
|
+
if cached is not None:
|
|
38
|
+
return cached, True
|
|
39
|
+
|
|
40
|
+
payload = {
|
|
41
|
+
"model": settings.ollama_model,
|
|
42
|
+
"prompt": PARSE_PROMPT + raw_text,
|
|
43
|
+
"stream": False,
|
|
44
|
+
"format": "json",
|
|
45
|
+
}
|
|
46
|
+
try:
|
|
47
|
+
async with httpx.AsyncClient(timeout=settings.ollama_timeout) as client:
|
|
48
|
+
resp = await client.post(f"{settings.ollama_url}/api/generate", json=payload)
|
|
49
|
+
if resp.status_code == 404:
|
|
50
|
+
raise ollama_model_not_found(settings.ollama_model)
|
|
51
|
+
resp.raise_for_status()
|
|
52
|
+
response_text = resp.json().get("response", "{}")
|
|
53
|
+
result = _safe_parse(response_text)
|
|
54
|
+
await cache_svc.cache_set(raw_text, result)
|
|
55
|
+
return result, False
|
|
56
|
+
except httpx.ConnectError:
|
|
57
|
+
raise ollama_unavailable(settings.ollama_url)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def parse_image(image_bytes: bytes, mime_type: str) -> tuple[dict, str, bool]:
|
|
61
|
+
"""Returns (parsed_dict, raw_text, was_cached)."""
|
|
62
|
+
cached = await cache_svc.cache_get(image_bytes)
|
|
63
|
+
if cached is not None:
|
|
64
|
+
return cached, "", True
|
|
65
|
+
|
|
66
|
+
b64 = base64.b64encode(image_bytes).decode()
|
|
67
|
+
payload = {
|
|
68
|
+
"model": settings.ollama_model,
|
|
69
|
+
"prompt": PARSE_PROMPT + "Extract all visible travel information from this image.",
|
|
70
|
+
"images": [b64],
|
|
71
|
+
"stream": False,
|
|
72
|
+
"format": "json",
|
|
73
|
+
}
|
|
74
|
+
try:
|
|
75
|
+
async with httpx.AsyncClient(timeout=settings.ollama_timeout) as client:
|
|
76
|
+
resp = await client.post(f"{settings.ollama_url}/api/generate", json=payload)
|
|
77
|
+
if resp.status_code == 404:
|
|
78
|
+
raise ollama_model_not_found(settings.ollama_model)
|
|
79
|
+
resp.raise_for_status()
|
|
80
|
+
response_text = resp.json().get("response", "{}")
|
|
81
|
+
result = _safe_parse(response_text)
|
|
82
|
+
await cache_svc.cache_set(image_bytes, result)
|
|
83
|
+
# For images, Ollama returns structured JSON directly — there is no
|
|
84
|
+
# separate OCR text output. Store the human-readable raw_summary
|
|
85
|
+
# (from the parsed result) as raw_text instead of the JSON blob.
|
|
86
|
+
return result, result.get("raw_summary") or None, False
|
|
87
|
+
except httpx.ConnectError:
|
|
88
|
+
raise ollama_unavailable(settings.ollama_url)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _safe_parse(text: str) -> dict:
|
|
92
|
+
try:
|
|
93
|
+
cleaned = re.sub(r"```(?:json)?\s*|\s*```", "", text).strip()
|
|
94
|
+
return json.loads(cleaned)
|
|
95
|
+
except json.JSONDecodeError:
|
|
96
|
+
return {
|
|
97
|
+
"type": "other",
|
|
98
|
+
"title": "Unrecognized document",
|
|
99
|
+
"raw_summary": text[:300],
|
|
100
|
+
"confidence": 0.0,
|
|
101
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Extracts raw text from various file types before sending to Ollama."""
|
|
2
|
+
import io
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import fitz # PyMuPDF
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def extract_text_from_pdf(content: bytes) -> str:
|
|
8
|
+
doc = fitz.open(stream=content, filetype="pdf")
|
|
9
|
+
pages = [page.get_text() for page in doc]
|
|
10
|
+
return "\n".join(pages).strip()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_image(mime_type: str) -> bool:
|
|
14
|
+
return mime_type.startswith("image/")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_pdf(mime_type: str) -> bool:
|
|
18
|
+
return mime_type == "application/pdf"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_text(mime_type: str) -> bool:
|
|
22
|
+
return mime_type.startswith("text/")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Freemium quota enforcement.
|
|
3
|
+
|
|
4
|
+
Plans:
|
|
5
|
+
free — 50 AI parses / day, 3 active trips
|
|
6
|
+
pro — unlimited parses, unlimited trips
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
await quota.check_parse(db, uid) # raises HTTP 429 if over limit
|
|
10
|
+
await quota.check_trips(db, uid) # raises HTTP 402 if over limit
|
|
11
|
+
await quota.get_plan(db, uid) # returns "free" | "pro"
|
|
12
|
+
"""
|
|
13
|
+
from datetime import datetime, timedelta, timezone
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
|
+
from sqlalchemy import text
|
|
16
|
+
from fastapi import HTTPException
|
|
17
|
+
|
|
18
|
+
FREE_DAILY_PARSES = 50
|
|
19
|
+
FREE_MAX_TRIPS = 3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _now() -> datetime:
|
|
23
|
+
return datetime.now(timezone.utc)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def get_plan(db: AsyncSession, uid: str) -> str:
|
|
27
|
+
row = await db.execute(
|
|
28
|
+
text("""
|
|
29
|
+
SELECT plan FROM users
|
|
30
|
+
WHERE id = :uid
|
|
31
|
+
AND (plan_expires_at IS NULL OR plan_expires_at > :now)
|
|
32
|
+
"""),
|
|
33
|
+
{"uid": uid, "now": _now()},
|
|
34
|
+
)
|
|
35
|
+
r = row.fetchone()
|
|
36
|
+
return r[0] if r else "free"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def _ensure_user(db: AsyncSession, uid: str) -> None:
|
|
40
|
+
await db.execute(
|
|
41
|
+
text("INSERT INTO users (id) VALUES (:uid) ON CONFLICT (id) DO NOTHING"),
|
|
42
|
+
{"uid": uid},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def check_parse(db: AsyncSession, uid: str) -> None:
|
|
47
|
+
await _ensure_user(db, uid)
|
|
48
|
+
plan = await get_plan(db, uid)
|
|
49
|
+
if plan != "free":
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
cutoff = _now() - timedelta(days=1)
|
|
53
|
+
row = await db.execute(
|
|
54
|
+
text("""
|
|
55
|
+
SELECT COUNT(*) FROM events
|
|
56
|
+
WHERE user_id = :uid
|
|
57
|
+
AND event_name IN ('parse_file', 'parse_text', 'parse_url')
|
|
58
|
+
AND created_at >= :cutoff
|
|
59
|
+
"""),
|
|
60
|
+
{"uid": uid, "cutoff": cutoff},
|
|
61
|
+
)
|
|
62
|
+
count = row.scalar() or 0
|
|
63
|
+
if count >= FREE_DAILY_PARSES:
|
|
64
|
+
raise HTTPException(
|
|
65
|
+
status_code=429,
|
|
66
|
+
detail=f"Free plan: {FREE_DAILY_PARSES} AI parses per day. Upgrade to Pro for unlimited access.",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def check_trips(db: AsyncSession, uid: str) -> None:
|
|
71
|
+
await _ensure_user(db, uid)
|
|
72
|
+
plan = await get_plan(db, uid)
|
|
73
|
+
if plan != "free":
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
row = await db.execute(
|
|
77
|
+
text("SELECT COUNT(*) FROM trips WHERE user_id = :uid"),
|
|
78
|
+
{"uid": uid},
|
|
79
|
+
)
|
|
80
|
+
count = row.scalar() or 0
|
|
81
|
+
if count >= FREE_MAX_TRIPS:
|
|
82
|
+
raise HTTPException(
|
|
83
|
+
status_code=402,
|
|
84
|
+
detail=f"Free plan: max {FREE_MAX_TRIPS} trips. Upgrade to Pro for unlimited trips.",
|
|
85
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
def init(dsn: str = "", environment: str = "production", release: str = "0.1.0") -> None:
|
|
2
|
+
if not dsn:
|
|
3
|
+
return
|
|
4
|
+
import sentry_sdk
|
|
5
|
+
sentry_sdk.init(
|
|
6
|
+
dsn=dsn,
|
|
7
|
+
environment=environment,
|
|
8
|
+
release=release,
|
|
9
|
+
traces_sample_rate=0.05,
|
|
10
|
+
profiles_sample_rate=0.01,
|
|
11
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fire-and-forget event tracking.
|
|
3
|
+
Never raises — telemetry must never break the request.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
from sqlalchemy import text
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def track(
|
|
12
|
+
db: AsyncSession,
|
|
13
|
+
user_id: str | None,
|
|
14
|
+
event_name: str,
|
|
15
|
+
properties: dict[str, Any] | None = None,
|
|
16
|
+
platform: str | None = None,
|
|
17
|
+
app_version: str | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
try:
|
|
20
|
+
await db.execute(
|
|
21
|
+
text("""
|
|
22
|
+
INSERT INTO events (user_id, event_name, properties, platform, app_version)
|
|
23
|
+
VALUES (:uid, :name, :props, :platform, :version)
|
|
24
|
+
"""),
|
|
25
|
+
{
|
|
26
|
+
"uid": user_id,
|
|
27
|
+
"name": event_name,
|
|
28
|
+
"props": json.dumps(properties or {}),
|
|
29
|
+
"platform": platform,
|
|
30
|
+
"version": app_version,
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
await db.commit()
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# shared monorepo package — installed from /packages/telegram-notify in Docker
|
|
2
|
+
# local dev: pip install -e ../../../../packages/telegram-notify
|
|
3
|
+
telegram-notify @ file:///packages/telegram-notify
|
|
4
|
+
fastapi==0.115.5
|
|
5
|
+
uvicorn[standard]==0.32.1
|
|
6
|
+
python-jose[cryptography]==3.3.0
|
|
7
|
+
python-multipart==0.0.12
|
|
8
|
+
httpx==0.28.0
|
|
9
|
+
asyncpg==0.30.0
|
|
10
|
+
sqlalchemy[asyncio]==2.0.36
|
|
11
|
+
alembic==1.14.0
|
|
12
|
+
python-dotenv==1.0.1
|
|
13
|
+
pydantic[email]==2.10.3
|
|
14
|
+
email-validator>=2.0
|
|
15
|
+
pydantic-settings==2.6.1
|
|
16
|
+
Pillow==11.0.0
|
|
17
|
+
PyMuPDF==1.24.14
|
|
18
|
+
aiofiles==24.1.0
|
|
19
|
+
python-magic==0.4.27
|
|
20
|
+
redis[asyncio]==5.2.1
|
|
21
|
+
slowapi>=0.1.9
|
|
22
|
+
limits>=3.6.0
|
|
23
|
+
sentry-sdk[fastapi]>=2.0
|
|
24
|
+
stripe>=8.0
|
|
25
|
+
reportlab>=4.0
|
|
File without changes
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared fixtures for Travel Chaos Organizer backend tests.
|
|
3
|
+
|
|
4
|
+
Patterns from cachly-mcp:
|
|
5
|
+
- In-memory mocks instead of real services (no Docker required for unit tests)
|
|
6
|
+
- Deterministic fake data via factory helpers
|
|
7
|
+
- Auth always injectable / bypassable in unit scope
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import uuid
|
|
12
|
+
from collections.abc import AsyncGenerator
|
|
13
|
+
from datetime import date
|
|
14
|
+
from typing import Any
|
|
15
|
+
from unittest.mock import AsyncMock, patch
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
import pytest_asyncio
|
|
19
|
+
from fastapi import FastAPI
|
|
20
|
+
from httpx import ASGITransport, AsyncClient
|
|
21
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
22
|
+
|
|
23
|
+
from app.db.database import Base, get_db
|
|
24
|
+
from app.main import app
|
|
25
|
+
from app.models.schemas import ParsedTravelData
|
|
26
|
+
|
|
27
|
+
# ── In-memory SQLite DB (mirrors cachly MockRedis pattern) ────────────────────
|
|
28
|
+
|
|
29
|
+
TEST_DB_URL = "sqlite+aiosqlite:///:memory:"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest_asyncio.fixture(scope="function")
|
|
33
|
+
async def db_engine():
|
|
34
|
+
import sqlalchemy
|
|
35
|
+
engine = create_async_engine(TEST_DB_URL, echo=False)
|
|
36
|
+
async with engine.begin() as conn:
|
|
37
|
+
# Execute each DDL statement separately — SQLite doesn't support
|
|
38
|
+
# multi-statement execute() calls.
|
|
39
|
+
for stmt in SQLITE_SCHEMA.strip().split(";"):
|
|
40
|
+
stmt = stmt.strip()
|
|
41
|
+
if stmt:
|
|
42
|
+
await conn.execute(sqlalchemy.text(stmt))
|
|
43
|
+
yield engine
|
|
44
|
+
await engine.dispose()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest_asyncio.fixture
|
|
48
|
+
async def db_session(db_engine) -> AsyncGenerator[AsyncSession, None]:
|
|
49
|
+
SessionLocal = async_sessionmaker(db_engine, expire_on_commit=False)
|
|
50
|
+
async with SessionLocal() as session:
|
|
51
|
+
yield session
|
|
52
|
+
await session.rollback()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── Auth bypass (like cachly's checkJwt mock) ─────────────────────────────────
|
|
56
|
+
|
|
57
|
+
FAKE_USER_ID = "user-test-123"
|
|
58
|
+
FAKE_USER_PAYLOAD = {"sub": FAKE_USER_ID, "email": "test@example.com"}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest_asyncio.fixture
|
|
62
|
+
async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
|
|
63
|
+
"""HTTP client with auth bypassed and real in-memory DB."""
|
|
64
|
+
from app.auth.keycloak import get_current_user
|
|
65
|
+
from app.db.database import get_db
|
|
66
|
+
|
|
67
|
+
from app.auth.keycloak import user_id_or_bot
|
|
68
|
+
|
|
69
|
+
async def override_auth():
|
|
70
|
+
return FAKE_USER_PAYLOAD
|
|
71
|
+
|
|
72
|
+
async def override_user_id_or_bot():
|
|
73
|
+
return FAKE_USER_ID
|
|
74
|
+
|
|
75
|
+
async def override_db():
|
|
76
|
+
yield db_session
|
|
77
|
+
|
|
78
|
+
app.dependency_overrides[get_current_user] = override_auth
|
|
79
|
+
app.dependency_overrides[user_id_or_bot] = override_user_id_or_bot
|
|
80
|
+
app.dependency_overrides[get_db] = override_db
|
|
81
|
+
|
|
82
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
|
83
|
+
yield c
|
|
84
|
+
|
|
85
|
+
app.dependency_overrides.clear()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ── Ollama mock (no real LLM needed) ──────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
PARSED_FLIGHT = ParsedTravelData(
|
|
91
|
+
type="flight",
|
|
92
|
+
title="LH 400 Frankfurt → New York",
|
|
93
|
+
booking_ref="ABC123",
|
|
94
|
+
provider="Lufthansa",
|
|
95
|
+
event_at="2024-08-15T10:30:00",
|
|
96
|
+
origin="FRA",
|
|
97
|
+
destination="JFK",
|
|
98
|
+
passengers=["Max Mustermann"],
|
|
99
|
+
price="€ 850",
|
|
100
|
+
confidence=0.92,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.fixture
|
|
105
|
+
def mock_ollama_flight():
|
|
106
|
+
with patch("app.services.ollama.parse_text", new_callable=AsyncMock) as m:
|
|
107
|
+
m.return_value = (PARSED_FLIGHT.model_dump(), False)
|
|
108
|
+
yield m
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.fixture
|
|
112
|
+
def mock_ollama_low_confidence():
|
|
113
|
+
with patch("app.services.ollama.parse_text", new_callable=AsyncMock) as m:
|
|
114
|
+
m.return_value = (
|
|
115
|
+
{"type": "other", "title": "Unrecognized document", "confidence": 0.1},
|
|
116
|
+
False,
|
|
117
|
+
)
|
|
118
|
+
yield m
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ── Factory helpers ────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
def make_trip(name: str = "Sommerurlaub 2024", **kwargs) -> dict[str, Any]:
|
|
124
|
+
return {"name": name, "description": "Test trip", "start_date": "2024-08-01",
|
|
125
|
+
"end_date": "2024-08-15", **kwargs}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def make_item(**kwargs) -> dict[str, Any]:
|
|
129
|
+
return {"type": "flight", "title": "LH 400 FRA→JFK", "event_at": "2024-08-15T10:30:00",
|
|
130
|
+
"booking_ref": "ABC123", "provider": "Lufthansa", **kwargs}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── SQLite-compatible schema (no PostgreSQL ENUMs / pgcrypto) ─────────────────
|
|
134
|
+
|
|
135
|
+
def _uuid_default() -> str:
|
|
136
|
+
"""SQLite UUID expression: generates xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format."""
|
|
137
|
+
return (
|
|
138
|
+
"lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-' || "
|
|
139
|
+
"lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' || "
|
|
140
|
+
"lower(hex(randomblob(6)))"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
_UD = _uuid_default()
|
|
144
|
+
|
|
145
|
+
SQLITE_SCHEMA = f"""
|
|
146
|
+
CREATE TABLE IF NOT EXISTS trips (
|
|
147
|
+
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
148
|
+
user_id TEXT NOT NULL,
|
|
149
|
+
name TEXT NOT NULL,
|
|
150
|
+
description TEXT,
|
|
151
|
+
start_date TEXT,
|
|
152
|
+
end_date TEXT,
|
|
153
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
154
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
CREATE TABLE IF NOT EXISTS trip_items (
|
|
158
|
+
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
159
|
+
trip_id TEXT NOT NULL,
|
|
160
|
+
user_id TEXT NOT NULL,
|
|
161
|
+
type TEXT NOT NULL DEFAULT 'other',
|
|
162
|
+
title TEXT NOT NULL,
|
|
163
|
+
raw_text TEXT,
|
|
164
|
+
parsed_data TEXT,
|
|
165
|
+
event_at TEXT,
|
|
166
|
+
event_end_at TEXT,
|
|
167
|
+
booking_ref TEXT,
|
|
168
|
+
provider TEXT,
|
|
169
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
170
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
CREATE TABLE IF NOT EXISTS attachments (
|
|
174
|
+
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
175
|
+
trip_item_id TEXT,
|
|
176
|
+
inbox_id TEXT,
|
|
177
|
+
user_id TEXT NOT NULL,
|
|
178
|
+
file_path TEXT NOT NULL,
|
|
179
|
+
file_name TEXT NOT NULL,
|
|
180
|
+
mime_type TEXT NOT NULL,
|
|
181
|
+
size_bytes INTEGER,
|
|
182
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
CREATE TABLE IF NOT EXISTS chaos_inbox (
|
|
186
|
+
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
187
|
+
user_id TEXT NOT NULL,
|
|
188
|
+
raw_content TEXT,
|
|
189
|
+
source TEXT,
|
|
190
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
191
|
+
parsed_data TEXT,
|
|
192
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
193
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
197
|
+
id TEXT PRIMARY KEY,
|
|
198
|
+
email TEXT,
|
|
199
|
+
plan TEXT NOT NULL DEFAULT 'free',
|
|
200
|
+
plan_expires_at TEXT,
|
|
201
|
+
stripe_customer_id TEXT,
|
|
202
|
+
telegram_chat_id TEXT UNIQUE,
|
|
203
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
204
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
208
|
+
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
209
|
+
user_id TEXT,
|
|
210
|
+
event_name TEXT NOT NULL,
|
|
211
|
+
properties TEXT,
|
|
212
|
+
platform TEXT,
|
|
213
|
+
app_version TEXT,
|
|
214
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
CREATE TABLE IF NOT EXISTS waitlist (
|
|
218
|
+
id TEXT PRIMARY KEY DEFAULT ({_UD}),
|
|
219
|
+
email TEXT NOT NULL UNIQUE,
|
|
220
|
+
source TEXT DEFAULT 'landing',
|
|
221
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
222
|
+
);
|
|
223
|
+
"""
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Tests for admin endpoints."""
|
|
2
|
+
import base64
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from httpx import AsyncClient
|
|
6
|
+
|
|
7
|
+
pytestmark = pytest.mark.asyncio
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def admin_headers(user: str = "admin", password: str = "changeme") -> dict:
|
|
11
|
+
token = base64.b64encode(f"{user}:{password}".encode()).decode()
|
|
12
|
+
return {"Authorization": f"Basic {token}"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def test_admin_health_requires_auth(client: AsyncClient):
|
|
16
|
+
r = await client.get("/admin/health")
|
|
17
|
+
assert r.status_code == 401
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def test_admin_health_ok(client: AsyncClient):
|
|
21
|
+
r = await client.get("/admin/health", headers=admin_headers())
|
|
22
|
+
assert r.status_code == 200
|
|
23
|
+
data = r.json()
|
|
24
|
+
assert "events_total" in data
|
|
25
|
+
assert "waitlist_total" in data
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def test_admin_dashboard_requires_auth(client: AsyncClient):
|
|
29
|
+
r = await client.get("/admin")
|
|
30
|
+
assert r.status_code == 401
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def test_admin_dashboard_returns_html(client: AsyncClient):
|
|
34
|
+
r = await client.get("/admin", headers=admin_headers())
|
|
35
|
+
assert r.status_code == 200
|
|
36
|
+
assert "text/html" in r.headers["content-type"]
|
|
37
|
+
assert "TCO Admin" in r.text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def test_admin_wrong_password(client: AsyncClient):
|
|
41
|
+
r = await client.get("/admin/health", headers=admin_headers(password="wrongpassword"))
|
|
42
|
+
assert r.status_code == 401
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def test_admin_events_summary(client: AsyncClient):
|
|
46
|
+
r = await client.get("/admin/events/summary", headers=admin_headers())
|
|
47
|
+
assert r.status_code == 200
|
|
48
|
+
assert "events" in r.json()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def test_admin_waitlist(client: AsyncClient):
|
|
52
|
+
r = await client.get("/admin/waitlist", headers=admin_headers())
|
|
53
|
+
assert r.status_code == 200
|
|
54
|
+
assert "signups" in r.json()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def test_admin_set_user_plan(client: AsyncClient, db_session):
|
|
58
|
+
from sqlalchemy import text
|
|
59
|
+
|
|
60
|
+
# First create the user by calling /me
|
|
61
|
+
await client.get("/api/v1/users/me")
|
|
62
|
+
|
|
63
|
+
r = await client.patch(
|
|
64
|
+
"/admin/users/user-test-123/plan?plan=pro", headers=admin_headers()
|
|
65
|
+
)
|
|
66
|
+
assert r.status_code == 200
|
|
67
|
+
assert r.json()["plan"] == "pro"
|
|
68
|
+
|
|
69
|
+
# Verify in DB
|
|
70
|
+
row = await db_session.execute(
|
|
71
|
+
text("SELECT plan FROM users WHERE id = 'user-test-123'")
|
|
72
|
+
)
|
|
73
|
+
assert row.fetchone()[0] == "pro"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def test_admin_set_invalid_plan(client: AsyncClient):
|
|
77
|
+
"""Setting an invalid plan returns 400."""
|
|
78
|
+
# Create user first
|
|
79
|
+
await client.get("/api/v1/users/me")
|
|
80
|
+
|
|
81
|
+
r = await client.patch(
|
|
82
|
+
"/admin/users/user-test-123/plan?plan=enterprise", headers=admin_headers()
|
|
83
|
+
)
|
|
84
|
+
assert r.status_code == 400
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def test_admin_users_list(client: AsyncClient):
|
|
88
|
+
r = await client.get("/admin/users", headers=admin_headers())
|
|
89
|
+
assert r.status_code == 200
|
|
90
|
+
assert "users" in r.json()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def test_admin_health_counts(client: AsyncClient):
|
|
94
|
+
"""Health endpoint returns all expected count fields."""
|
|
95
|
+
r = await client.get("/admin/health", headers=admin_headers())
|
|
96
|
+
assert r.status_code == 200
|
|
97
|
+
data = r.json()
|
|
98
|
+
assert "users_total" in data
|
|
99
|
+
assert "pro_users" in data
|