@cachly-dev/mcp-server 0.10.65 → 0.10.67

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.
Files changed (277) hide show
  1. package/dist/src/embeddings.js +10 -0
  2. package/dist/src/embeddings.js.map +1 -1
  3. package/dist/src/handlers/brain.js +2 -1
  4. package/dist/src/handlers/brain.js.map +1 -1
  5. package/dist/src/handlers/cache.js +4 -0
  6. package/dist/src/handlers/cache.js.map +1 -1
  7. package/dist/src/handlers/tco.js +1 -0
  8. package/dist/src/handlers/tco.js.map +1 -1
  9. package/dist/src/index.js +2 -2
  10. package/dist/src/index.js.map +1 -1
  11. package/package.json +7 -1
  12. package/server.json +2 -2
  13. package/.env.test.example +0 -23
  14. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  15. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  16. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  17. package/.github/workflows/eas-build-android.yml +0 -58
  18. package/.github/workflows/eas-update.yml +0 -55
  19. package/.github/workflows/publish.yml +0 -72
  20. package/BENCH.md +0 -113
  21. package/CHANGELOG.md +0 -653
  22. package/Dockerfile +0 -16
  23. package/LAUNCH_TEXTS.md +0 -182
  24. package/PROGRESS.md +0 -141
  25. package/STRATEGY.md +0 -228
  26. package/VISION_10X.md +0 -133
  27. package/apps/travel-chaos-organizer/.env.example +0 -49
  28. package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
  29. package/apps/travel-chaos-organizer/Makefile +0 -57
  30. package/apps/travel-chaos-organizer/README.md +0 -159
  31. package/apps/travel-chaos-organizer/backend/.env.example +0 -10
  32. package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
  33. package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
  34. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
  35. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  36. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
  37. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
  38. package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
  39. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  40. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  41. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
  42. package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
  43. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  44. package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
  45. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
  46. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
  47. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
  48. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
  49. package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
  50. package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
  51. package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
  52. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  53. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
  54. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  55. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
  56. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
  57. package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
  58. package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
  59. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
  60. package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
  61. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
  62. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
  63. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
  64. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
  65. package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
  66. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
  67. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  68. package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
  69. package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
  70. package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
  71. package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
  72. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
  73. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
  74. package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
  75. package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
  76. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
  77. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
  78. package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
  79. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
  80. package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
  81. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  82. package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
  83. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
  84. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
  85. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
  86. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
  87. package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
  88. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
  89. package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
  90. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
  91. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
  92. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
  93. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
  94. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
  95. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
  96. package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
  97. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
  98. package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
  99. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  100. package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
  101. package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
  102. package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
  103. package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
  104. package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
  105. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
  106. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
  107. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
  108. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
  109. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
  110. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
  111. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
  112. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
  113. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
  114. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
  115. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
  116. package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
  117. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
  118. package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
  119. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
  120. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
  121. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
  122. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
  123. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
  124. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
  125. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
  126. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
  127. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
  128. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
  129. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
  130. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
  131. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
  132. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
  133. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
  134. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
  135. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
  136. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
  137. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
  138. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
  139. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
  140. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
  141. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
  142. package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
  143. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
  144. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
  145. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
  146. package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
  147. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
  148. package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
  149. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
  150. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
  151. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
  152. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
  153. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
  154. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
  155. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
  156. package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
  157. package/apps/travel-chaos-organizer/frontend/package.json +0 -44
  158. package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
  159. package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
  160. package/apps/travel-chaos-organizer/landing/index.html +0 -773
  161. package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
  162. package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
  163. package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
  164. package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
  165. package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
  166. package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
  167. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
  168. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
  169. package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
  170. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
  171. package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
  172. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
  173. package/dist/src/__tests__/auth.test.d.ts +0 -1
  174. package/dist/src/__tests__/auth.test.js +0 -107
  175. package/dist/src/__tests__/auth.test.js.map +0 -1
  176. package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
  177. package/dist/src/__tests__/brain-flow.test.js +0 -1241
  178. package/dist/src/__tests__/brain-flow.test.js.map +0 -1
  179. package/dist/src/__tests__/confidence.test.d.ts +0 -13
  180. package/dist/src/__tests__/confidence.test.js +0 -233
  181. package/dist/src/__tests__/confidence.test.js.map +0 -1
  182. package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
  183. package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
  184. package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
  185. package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
  186. package/dist/src/__tests__/e2e/api-health.test.js +0 -95
  187. package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
  188. package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
  189. package/dist/src/__tests__/e2e/cli.test.js +0 -204
  190. package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
  191. package/dist/src/__tests__/e2e/config.d.ts +0 -35
  192. package/dist/src/__tests__/e2e/config.js +0 -53
  193. package/dist/src/__tests__/e2e/config.js.map +0 -1
  194. package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
  195. package/dist/src/__tests__/e2e/funnel.test.js +0 -90
  196. package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
  197. package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
  198. package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
  199. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
  200. package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
  201. package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
  202. package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
  203. package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
  204. package/dist/src/__tests__/handlers-unit.test.js +0 -505
  205. package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
  206. package/dist/src/__tests__/namespace.test.d.ts +0 -1
  207. package/dist/src/__tests__/namespace.test.js +0 -114
  208. package/dist/src/__tests__/namespace.test.js.map +0 -1
  209. package/dist/src/__tests__/rerank.test.d.ts +0 -1
  210. package/dist/src/__tests__/rerank.test.js +0 -125
  211. package/dist/src/__tests__/rerank.test.js.map +0 -1
  212. package/dist/src/__tests__/utils.test.d.ts +0 -1
  213. package/dist/src/__tests__/utils.test.js +0 -83
  214. package/dist/src/__tests__/utils.test.js.map +0 -1
  215. package/dist/src/bench/cachly-bench.d.ts +0 -39
  216. package/dist/src/bench/cachly-bench.js +0 -223
  217. package/dist/src/bench/cachly-bench.js.map +0 -1
  218. package/dist/src/bench/fixtures.d.ts +0 -37
  219. package/dist/src/bench/fixtures.js +0 -225
  220. package/dist/src/bench/fixtures.js.map +0 -1
  221. package/dist/src/search.test.d.ts +0 -1
  222. package/dist/src/search.test.js +0 -713
  223. package/dist/src/search.test.js.map +0 -1
  224. package/eslint.config.js +0 -16
  225. package/glama.json +0 -36
  226. package/llms.txt +0 -155
  227. package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
  228. package/packages/telegram-notify/client.ts +0 -88
  229. package/packages/telegram-notify/emojis.json +0 -22
  230. package/packages/telegram-notify/pyproject.toml +0 -17
  231. package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
  232. package/packages/telegram-notify/telegram_notify/client.py +0 -87
  233. package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
  234. package/scripts/e2e-run.sh +0 -160
  235. package/scripts/generate-icons.js +0 -28
  236. package/smithery.yaml +0 -45
  237. package/src/__tests__/auth.test.ts +0 -147
  238. package/src/__tests__/brain-flow.test.ts +0 -1472
  239. package/src/__tests__/confidence.test.ts +0 -285
  240. package/src/__tests__/e2e/api-auth.test.ts +0 -182
  241. package/src/__tests__/e2e/api-health.test.ts +0 -103
  242. package/src/__tests__/e2e/cli.test.ts +0 -228
  243. package/src/__tests__/e2e/config.ts +0 -53
  244. package/src/__tests__/e2e/funnel.test.ts +0 -99
  245. package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
  246. package/src/__tests__/e2e/provisioning.test.ts +0 -128
  247. package/src/__tests__/handlers-unit.test.ts +0 -677
  248. package/src/__tests__/namespace.test.ts +0 -126
  249. package/src/__tests__/rerank.test.ts +0 -141
  250. package/src/__tests__/utils.test.ts +0 -81
  251. package/src/auth.ts +0 -46
  252. package/src/bench/cachly-bench.ts +0 -263
  253. package/src/bench/fixtures.ts +0 -256
  254. package/src/ckg.ts +0 -103
  255. package/src/confidence.ts +0 -38
  256. package/src/embeddings.ts +0 -217
  257. package/src/handlers/advanced.ts +0 -647
  258. package/src/handlers/brain.ts +0 -2622
  259. package/src/handlers/cache.ts +0 -633
  260. package/src/handlers/context.ts +0 -222
  261. package/src/handlers/fedbrain.ts +0 -1232
  262. package/src/handlers/instances.ts +0 -180
  263. package/src/handlers/roadmap.ts +0 -201
  264. package/src/handlers/syndicate.ts +0 -460
  265. package/src/handlers/tco.ts +0 -345
  266. package/src/handlers/team.ts +0 -1249
  267. package/src/index.ts +0 -2815
  268. package/src/namespace.ts +0 -128
  269. package/src/notifier.ts +0 -5
  270. package/src/rerank.ts +0 -148
  271. package/src/search.test.ts +0 -848
  272. package/src/search.ts +0 -1781
  273. package/src/tools.ts +0 -2056
  274. package/src/utils.ts +0 -90
  275. package/test-features.cjs +0 -23
  276. package/tsconfig.json +0 -16
  277. package/vitest.config.ts +0 -14
@@ -1,179 +0,0 @@
1
- """
2
- Stripe payment endpoints.
3
- POST /api/v1/payments/checkout — create Stripe Checkout session for Pro upgrade
4
- POST /api/v1/payments/webhook — handle Stripe webhook events
5
- """
6
- from typing import Annotated
7
- from fastapi import APIRouter, Depends, HTTPException, Request
8
- from sqlalchemy.ext.asyncio import AsyncSession
9
- from sqlalchemy import text
10
- from datetime import datetime, timedelta, timezone
11
- from app.auth.keycloak import user_id
12
- from app.db.database import get_db
13
- from app.config import get_settings
14
- from app.limiter import limiter
15
-
16
- router = APIRouter(prefix="/payments", tags=["payments"])
17
-
18
-
19
- @limiter.limit("5/minute")
20
- @router.post("/checkout")
21
- async def create_checkout(
22
- request: Request,
23
- uid: Annotated[str, Depends(user_id)],
24
- db: Annotated[AsyncSession, Depends(get_db)],
25
- ):
26
- s = get_settings()
27
- if not s.stripe_secret_key:
28
- raise HTTPException(status_code=503, detail="Payments not configured")
29
-
30
- import stripe
31
- stripe.api_key = s.stripe_secret_key
32
-
33
- # Get user email from DB if available
34
- row = await db.execute(text("SELECT email FROM users WHERE id = :uid"), {"uid": uid})
35
- r = row.fetchone()
36
- customer_email = r[0] if r and r[0] else None
37
-
38
- session = stripe.checkout.Session.create(
39
- mode="subscription",
40
- line_items=[{"price": s.stripe_pro_price_id, "quantity": 1}],
41
- success_url=s.stripe_success_url + "?session_id={CHECKOUT_SESSION_ID}",
42
- cancel_url=s.stripe_cancel_url,
43
- client_reference_id=uid,
44
- **({"customer_email": customer_email} if customer_email else {}),
45
- metadata={"user_id": uid},
46
- )
47
- from app.services import telemetry
48
- await telemetry.track(db, uid, "stripe_checkout_initiated", {"price_id": s.stripe_pro_price_id})
49
- return {"checkout_url": session.url, "session_id": session.id}
50
-
51
-
52
- @router.post("/webhook")
53
- async def stripe_webhook(request: Request, db: Annotated[AsyncSession, Depends(get_db)]):
54
- s = get_settings()
55
- if not s.stripe_secret_key:
56
- return {"ok": True}
57
-
58
- import stripe
59
- stripe.api_key = s.stripe_secret_key
60
-
61
- payload = await request.body()
62
- sig = request.headers.get("stripe-signature", "")
63
- if not sig:
64
- raise HTTPException(status_code=400, detail="Missing Stripe-Signature header")
65
-
66
- try:
67
- event = stripe.Webhook.construct_event(payload, sig, s.stripe_webhook_secret)
68
- except Exception:
69
- raise HTTPException(status_code=400, detail="Invalid webhook signature")
70
-
71
- if event["type"] == "checkout.session.completed":
72
- obj = event["data"]["object"]
73
- uid = obj.get("metadata", {}).get("user_id") or obj.get("client_reference_id")
74
- if uid:
75
- expires_at = datetime.now(timezone.utc) + timedelta(days=365)
76
- await db.execute(
77
- text("""
78
- INSERT INTO users (id, plan, plan_expires_at, updated_at)
79
- VALUES (:uid, 'pro', :exp, :now)
80
- ON CONFLICT (id) DO UPDATE
81
- SET plan = 'pro', plan_expires_at = EXCLUDED.plan_expires_at,
82
- updated_at = EXCLUDED.updated_at
83
- """),
84
- {"uid": uid, "exp": expires_at, "now": datetime.now(timezone.utc)},
85
- )
86
- await db.commit()
87
- # Store Stripe customer ID for future subscription management
88
- stripe_cust = obj.get("customer")
89
- if stripe_cust:
90
- await db.execute(
91
- text("UPDATE users SET stripe_customer_id = :cid WHERE id = :uid"),
92
- {"cid": stripe_cust, "uid": uid},
93
- )
94
- await db.commit()
95
- from app.services import telemetry as tel_svc
96
- from app.services import notifier as notifier_svc
97
- await tel_svc.track(db, uid, "stripe_payment_completed", {"amount": obj.get("amount_total"), "currency": obj.get("currency")})
98
- await notifier_svc.notify("tco", "stripe_payment", {"user_id": uid, "amount": obj.get("amount_total"), "currency": obj.get("currency")})
99
- await notifier_svc.notify_user(db, uid, "🎉 *Du bist jetzt Pro!*\n\nAlle Features sind freigeschaltet. Viel Spaß beim Reisen ✈️")
100
-
101
- elif event["type"] in ("customer.subscription.deleted", "customer.subscription.paused"):
102
- cust_id = event["data"]["object"].get("customer")
103
- if cust_id:
104
- result = await db.execute(
105
- text("SELECT id FROM users WHERE stripe_customer_id = :cid"),
106
- {"cid": cust_id},
107
- )
108
- row = result.fetchone()
109
- if row:
110
- downgrade_uid = row[0]
111
- await db.execute(
112
- text("""
113
- UPDATE users SET plan = 'free', plan_expires_at = NULL, updated_at = :now
114
- WHERE id = :uid
115
- """),
116
- {"uid": downgrade_uid, "now": datetime.now(timezone.utc)},
117
- )
118
- await db.commit()
119
- from app.services import telemetry as tel_svc, notifier as notifier_svc
120
- await tel_svc.track(db, downgrade_uid, "stripe_subscription_cancelled", {"customer": cust_id})
121
- await notifier_svc.notify_user(db, downgrade_uid, "ℹ️ *Dein Pro-Abo wurde beendet.*\n\nDu bist jetzt wieder im Free-Plan. Deine Daten bleiben erhalten.")
122
- await notifier_svc.notify("tco", "stripe_payment", {"event": "cancelled", "user_id": downgrade_uid[:8]})
123
-
124
- elif event["type"] == "invoice.payment_succeeded":
125
- # Extend plan by 1 year on renewal
126
- sub = event["data"]["object"].get("subscription")
127
- cust_id = event["data"]["object"].get("customer")
128
- if sub and cust_id:
129
- result = await db.execute(
130
- text("SELECT id, plan_expires_at FROM users WHERE stripe_customer_id = :cid"),
131
- {"cid": cust_id},
132
- )
133
- row = result.fetchone()
134
- if row:
135
- renewal_uid = row[0]
136
- new_expiry = datetime.now(timezone.utc) + timedelta(days=366)
137
- await db.execute(
138
- text("""
139
- UPDATE users SET plan = 'pro', plan_expires_at = :exp, updated_at = :now
140
- WHERE id = :uid
141
- """),
142
- {"uid": renewal_uid, "exp": new_expiry, "now": datetime.now(timezone.utc)},
143
- )
144
- await db.commit()
145
-
146
- elif event["type"] == "invoice.payment_failed":
147
- cust_id = event["data"]["object"].get("customer")
148
- attempt_count = event["data"]["object"].get("attempt_count", 1)
149
- if cust_id:
150
- result = await db.execute(
151
- text("SELECT id, email FROM users WHERE stripe_customer_id = :cid"),
152
- {"cid": cust_id},
153
- )
154
- row = result.fetchone()
155
- if row:
156
- failed_uid, user_email = row[0], row[1]
157
- if attempt_count >= 3:
158
- # Too many failures — downgrade to free immediately
159
- await db.execute(
160
- text("""
161
- UPDATE users SET plan = 'free', plan_expires_at = NULL, updated_at = :now
162
- WHERE id = :uid
163
- """),
164
- {"uid": failed_uid, "now": datetime.now(timezone.utc)},
165
- )
166
- await db.commit()
167
- # else: keep pro, Stripe will retry (grace period)
168
- from app.services import telemetry as tel_svc, notifier as notifier_svc
169
- await tel_svc.track(db, failed_uid, "stripe_payment_failed", {"customer": cust_id, "attempt_count": attempt_count})
170
- await notifier_svc.notify("tco", "stripe_payment", {"event": "payment_failed", "user_id": failed_uid[:8], "attempt": attempt_count})
171
- if attempt_count < 3:
172
- await notifier_svc.notify_user(db, failed_uid, f"⚠️ *Zahlung fehlgeschlagen* (Versuch {attempt_count}/3)\n\nBitte aktualisiere deine Zahlungsmethode in Stripe. Dein Pro-Zugang bleibt noch aktiv.")
173
- else:
174
- await notifier_svc.notify_user(db, failed_uid, "🔴 *Pro-Zugang deaktiviert*\n\nNach 3 fehlgeschlagenen Zahlungsversuchen wurde dein Abo beendet. Bitte erneuere dein Abo in der App.")
175
- if user_email and attempt_count == 1:
176
- # TODO: send payment failure email via notifier_svc or a dedicated email service
177
- pass
178
-
179
- return {"ok": True}
@@ -1,134 +0,0 @@
1
- from typing import Annotated
2
- from uuid import UUID
3
- from fastapi import APIRouter, Depends, HTTPException, Request, status
4
- from sqlalchemy import text
5
- from sqlalchemy.ext.asyncio import AsyncSession
6
- from app.auth.keycloak import user_id, user_id_or_bot
7
- from app.db.database import get_db
8
- from app.models.schemas import TripCreate, TripOut, TripUpdate
9
- from app.services import quota
10
- from app.limiter import limiter
11
-
12
- router = APIRouter(prefix="/trips", tags=["trips"])
13
-
14
-
15
- @router.get("", response_model=list[TripOut])
16
- async def list_trips(
17
- uid: Annotated[str, Depends(user_id_or_bot)],
18
- db: Annotated[AsyncSession, Depends(get_db)],
19
- ):
20
- result = await db.execute(
21
- text("SELECT * FROM trips WHERE user_id = :uid ORDER BY start_date ASC, created_at DESC"),
22
- {"uid": uid},
23
- )
24
- return [dict(r._mapping) for r in result.fetchall()]
25
-
26
-
27
- @router.get("/search", response_model=list[TripOut])
28
- async def search_trips(
29
- uid: Annotated[str, Depends(user_id)],
30
- db: Annotated[AsyncSession, Depends(get_db)],
31
- q: str = "",
32
- ):
33
- if not q.strip():
34
- return []
35
- pattern = f"%{q.strip()}%"
36
- result = await db.execute(
37
- text("""
38
- SELECT DISTINCT t.* FROM trips t
39
- LEFT JOIN trip_items ti ON ti.trip_id = t.id
40
- WHERE t.user_id = :uid
41
- AND (
42
- t.name LIKE :p OR t.description LIKE :p
43
- OR ti.title LIKE :p OR ti.provider LIKE :p
44
- OR ti.booking_ref LIKE :p
45
- )
46
- ORDER BY t.start_date ASC, t.created_at DESC
47
- """),
48
- {"uid": uid, "p": pattern},
49
- )
50
- rows = [dict(r._mapping) for r in result.fetchall()]
51
- from app.services import telemetry
52
- await telemetry.track(db, uid, "trip_search", {"q": q.strip()[:100], "result_count": len(rows)})
53
- return rows
54
-
55
-
56
- @limiter.limit("30/minute")
57
- @router.post("", response_model=TripOut, status_code=status.HTTP_201_CREATED)
58
- async def create_trip(
59
- request: Request,
60
- body: TripCreate,
61
- uid: Annotated[str, Depends(user_id)],
62
- db: Annotated[AsyncSession, Depends(get_db)],
63
- ):
64
- await quota.check_trips(db, uid)
65
- result = await db.execute(
66
- text("""
67
- INSERT INTO trips (user_id, name, description, start_date, end_date)
68
- VALUES (:uid, :name, :desc, :start_date, :end_date)
69
- RETURNING *
70
- """),
71
- {"uid": uid, "name": body.name, "desc": body.description,
72
- "start_date": body.start_date, "end_date": body.end_date},
73
- )
74
- await db.commit()
75
- return dict(result.fetchone()._mapping)
76
-
77
-
78
- @router.get("/{trip_id}", response_model=TripOut)
79
- async def get_trip(
80
- trip_id: UUID,
81
- uid: Annotated[str, Depends(user_id)],
82
- db: Annotated[AsyncSession, Depends(get_db)],
83
- ):
84
- result = await db.execute(
85
- text("SELECT * FROM trips WHERE id = :id AND user_id = :uid"),
86
- {"id": str(trip_id), "uid": uid},
87
- )
88
- row = result.fetchone()
89
- if not row:
90
- raise HTTPException(status_code=404, detail="Trip not found")
91
- return dict(row._mapping)
92
-
93
-
94
- @limiter.limit("30/minute")
95
- @router.patch("/{trip_id}", response_model=TripOut)
96
- async def update_trip(
97
- request: Request,
98
- trip_id: UUID,
99
- body: TripUpdate,
100
- uid: Annotated[str, Depends(user_id)],
101
- db: Annotated[AsyncSession, Depends(get_db)],
102
- ):
103
- updates = body.model_dump(exclude_none=True)
104
- if not updates:
105
- raise HTTPException(status_code=400, detail="Nothing to update")
106
-
107
- set_clauses = ", ".join(f"{k} = :{k}" for k in updates)
108
- updates.update({"id": str(trip_id), "uid": uid})
109
- result = await db.execute(
110
- text(f"UPDATE trips SET {set_clauses}, updated_at = CURRENT_TIMESTAMP WHERE id = :id AND user_id = :uid RETURNING *"),
111
- updates,
112
- )
113
- row = result.fetchone()
114
- if not row:
115
- raise HTTPException(status_code=404, detail="Trip not found")
116
- await db.commit()
117
- return dict(row._mapping)
118
-
119
-
120
- @limiter.limit("30/minute")
121
- @router.delete("/{trip_id}", status_code=status.HTTP_204_NO_CONTENT)
122
- async def delete_trip(
123
- request: Request,
124
- trip_id: UUID,
125
- uid: Annotated[str, Depends(user_id)],
126
- db: Annotated[AsyncSession, Depends(get_db)],
127
- ):
128
- result = await db.execute(
129
- text("DELETE FROM trips WHERE id = :id AND user_id = :uid"),
130
- {"id": str(trip_id), "uid": uid},
131
- )
132
- await db.commit()
133
- if result.rowcount == 0:
134
- raise HTTPException(status_code=404, detail="Trip not found")
@@ -1,169 +0,0 @@
1
- """
2
- User profile endpoint — returns current user's plan info and upserts user row.
3
- """
4
- import secrets
5
- from typing import Annotated
6
- from datetime import datetime, timedelta, timezone
7
- from fastapi import APIRouter, Depends, HTTPException, Request
8
- from sqlalchemy.ext.asyncio import AsyncSession
9
- from sqlalchemy import text
10
- from app.auth.keycloak import user_id
11
- from app.db.database import get_db
12
-
13
- # In-process PIN store (sufficient for single-process; use Redis for multi-replica)
14
- _pending_links: dict[str, tuple[str, datetime]] = {} # pin -> (uid, expires_at)
15
-
16
- router = APIRouter(prefix="/users", tags=["users"])
17
-
18
- @router.get("/me")
19
- async def me(
20
- uid: Annotated[str, Depends(user_id)],
21
- db: Annotated[AsyncSession, Depends(get_db)],
22
- ):
23
- # Upsert user row (first login creates it)
24
- await db.execute(
25
- text("""
26
- INSERT INTO users (id) VALUES (:uid)
27
- ON CONFLICT (id) DO NOTHING
28
- """),
29
- {"uid": uid},
30
- )
31
- await db.commit()
32
-
33
- row_check = await db.execute(text("SELECT created_at, updated_at FROM users WHERE id = :uid"), {"uid": uid})
34
- user_row = row_check.fetchone()
35
- # Track first login (created_at == updated_at means fresh insert)
36
- if user_row and str(user_row[0])[:19] == str(user_row[1])[:19]:
37
- from app.services import telemetry, notifier
38
- await telemetry.track(db, uid, "user_first_login", {"uid": uid})
39
- await notifier.notify("tco", "new_user", {"uid": uid[:8] + "..."})
40
-
41
- row = await db.execute(
42
- text("""
43
- SELECT id, email, plan, plan_expires_at, created_at, telegram_chat_id
44
- FROM users WHERE id = :uid
45
- """),
46
- {"uid": uid},
47
- )
48
- r = row.fetchone()
49
- now = datetime.now(timezone.utc)
50
- plan = r[2] if r else "free"
51
- plan_expires_at = r[3] if r else None
52
- telegram_linked = bool(r[5]) if r else False
53
- # plan is only active if not expired
54
- if plan != "free" and plan_expires_at is not None:
55
- if isinstance(plan_expires_at, str):
56
- from datetime import datetime as _dt
57
- try:
58
- plan_expires_at_dt = _dt.fromisoformat(plan_expires_at.replace("Z", "+00:00"))
59
- except ValueError:
60
- plan_expires_at_dt = None
61
- else:
62
- plan_expires_at_dt = plan_expires_at
63
- if plan_expires_at_dt and plan_expires_at_dt < now:
64
- plan = "free"
65
-
66
- return {
67
- "id": uid,
68
- "plan": plan,
69
- "plan_expires_at": plan_expires_at,
70
- "free_daily_parses": 50,
71
- "free_max_trips": 3,
72
- "is_pro": plan != "free",
73
- "telegram_linked": telegram_linked,
74
- }
75
-
76
-
77
- @router.post("/telegram-pin")
78
- async def generate_telegram_pin(
79
- uid: Annotated[str, Depends(user_id)],
80
- ):
81
- """Generate a 6-digit PIN valid 10 min to link Telegram account."""
82
- pin = str(secrets.randbelow(900000) + 100000) # 100000-999999
83
- expires = datetime.now(timezone.utc) + timedelta(minutes=10)
84
- _pending_links[pin] = (uid, expires)
85
- return {"pin": pin, "expires_in_seconds": 600}
86
-
87
-
88
- @router.post("/telegram-link")
89
- async def confirm_telegram_link(
90
- pin: str,
91
- chat_id: str,
92
- request: Request,
93
- db: Annotated[AsyncSession, Depends(get_db)],
94
- ):
95
- """Called by the bot: validates PIN and stores chat_id. Requires X-Bot-Token header."""
96
- from app.config import get_settings as _gs
97
- s = _gs()
98
- bot_token = request.headers.get("X-Bot-Token", "")
99
- if not s.telegram_bot_token or bot_token != s.telegram_bot_token:
100
- raise HTTPException(status_code=401, detail="Unauthorized")
101
- entry = _pending_links.get(pin)
102
- if not entry:
103
- raise HTTPException(status_code=404, detail="PIN not found or expired")
104
- uid, expires = entry
105
- if datetime.now(timezone.utc) > expires:
106
- del _pending_links[pin]
107
- raise HTTPException(status_code=410, detail="PIN expired")
108
- del _pending_links[pin]
109
- await db.execute(
110
- text("UPDATE users SET telegram_chat_id = :cid WHERE id = :uid"),
111
- {"cid": chat_id, "uid": uid},
112
- )
113
- await db.commit()
114
- return {"ok": True, "uid": uid}
115
-
116
-
117
- from pydantic import BaseModel as _BaseModel
118
-
119
- class TelegramNotifyBody(_BaseModel):
120
- message: str
121
-
122
-
123
- @router.post("/telegram-notify")
124
- async def telegram_notify(
125
- uid: Annotated[str, Depends(user_id)],
126
- db: Annotated[AsyncSession, Depends(get_db)],
127
- body: TelegramNotifyBody,
128
- ):
129
- """Send a Telegram message to the current user's linked chat.
130
- Used by MCP tools and external integrations."""
131
- from app.services import notifier as notifier_svc
132
- await notifier_svc.notify_user(db, uid, body.message)
133
- return {"ok": True}
134
-
135
-
136
- @router.delete("/telegram-unlink")
137
- async def telegram_unlink(
138
- uid: Annotated[str, Depends(user_id)],
139
- db: Annotated[AsyncSession, Depends(get_db)],
140
- ):
141
- """Unlink Telegram from the current user account."""
142
- await db.execute(
143
- text("UPDATE users SET telegram_chat_id = NULL WHERE id = :uid"),
144
- {"uid": uid},
145
- )
146
- await db.commit()
147
- return {"ok": True}
148
-
149
-
150
- @router.get("/by-telegram/{chat_id}")
151
- async def get_user_by_telegram(
152
- chat_id: str,
153
- request: Request,
154
- db: Annotated[AsyncSession, Depends(get_db)],
155
- ):
156
- """Bot-internal: get user's trips by telegram chat_id. Authenticated by bot token header."""
157
- from app.config import get_settings
158
- s = get_settings()
159
- bot_token = request.headers.get("X-Bot-Token", "")
160
- if not s.telegram_bot_token or bot_token != s.telegram_bot_token:
161
- raise HTTPException(status_code=401, detail="Unauthorized")
162
- result = await db.execute(
163
- text("SELECT id, email, plan FROM users WHERE telegram_chat_id = :cid"),
164
- {"cid": chat_id},
165
- )
166
- row = result.fetchone()
167
- if not row:
168
- raise HTTPException(status_code=404, detail="Not linked")
169
- return {"id": row[0], "email": row[1], "plan": row[2]}
@@ -1,37 +0,0 @@
1
- from typing import Annotated
2
- from fastapi import APIRouter, Depends, Request
3
- from pydantic import BaseModel, EmailStr
4
- from sqlalchemy.ext.asyncio import AsyncSession
5
- from sqlalchemy import text
6
- from app.db.database import get_db
7
- from app.services import notifier, email as email_svc
8
- from app.limiter import limiter
9
-
10
- router = APIRouter(prefix="/waitlist", tags=["waitlist"])
11
-
12
-
13
- class WaitlistBody(BaseModel):
14
- email: EmailStr
15
- source: str = "landing"
16
-
17
-
18
- @limiter.limit("5/minute")
19
- @router.post("")
20
- async def join_waitlist(
21
- request: Request,
22
- body: WaitlistBody,
23
- db: Annotated[AsyncSession, Depends(get_db)],
24
- ):
25
- try:
26
- await db.execute(
27
- text("INSERT INTO waitlist (email, source) VALUES (:email, :source)"),
28
- {"email": body.email.lower(), "source": body.source},
29
- )
30
- await db.commit()
31
- # Fire-and-forget: notification + welcome email
32
- await notifier.notify("tco", "waitlist_signup", {"email": body.email.lower(), "source": body.source})
33
- await email_svc.send_waitlist_welcome(body.email.lower())
34
- return {"joined": True}
35
- except Exception:
36
- await db.rollback()
37
- return {"already_joined": True}
@@ -1,131 +0,0 @@
1
- """
2
- Cachly Redis cache for Ollama parse results.
3
-
4
- When CACHLY_REDIS_URL is configured, parsed documents are stored by
5
- content hash (SHA-256). Uploading the same PDF or text twice returns
6
- the cached result instantly — no second AI call.
7
-
8
- Hit/miss counters are stored in Redis (tco:stats:hits / tco:stats:misses)
9
- so they survive restarts and are visible in the /api/v1/cache/stats endpoint.
10
-
11
- Cache key format: tco:parse:{hex16}
12
- Stats keys: tco:stats:hits, tco:stats:misses
13
- TTL: 7 days (parse results are deterministic for the same input)
14
- """
15
- import hashlib
16
- import json
17
- from typing import Any
18
-
19
- _redis_client: Any = None
20
- _redis_available = False
21
-
22
- CACHE_TTL = 7 * 24 * 3600 # 7 days
23
-
24
-
25
- def _get_client():
26
- """Lazy-init Redis client. Returns None if Cachly is not configured."""
27
- global _redis_client, _redis_available
28
- if _redis_client is not None:
29
- return _redis_client if _redis_available else None
30
-
31
- import os
32
- redis_url = os.getenv("CACHLY_REDIS_URL", "")
33
- if not redis_url:
34
- _redis_available = False
35
- return None
36
-
37
- try:
38
- import redis.asyncio as aioredis
39
- _redis_client = aioredis.from_url(redis_url, decode_responses=True, socket_connect_timeout=3)
40
- _redis_available = True
41
- return _redis_client
42
- except Exception:
43
- _redis_available = False
44
- return None
45
-
46
-
47
- def _cache_key(content: str | bytes) -> str:
48
- if isinstance(content, str):
49
- content = content.encode()
50
- digest = hashlib.sha256(content).hexdigest()[:16]
51
- return f"tco:parse:{digest}"
52
-
53
-
54
- async def cache_get(content: str | bytes) -> dict | None:
55
- """Return cached parse result for this content, or None on miss/error.
56
- Increments tco:stats:hits on hit, tco:stats:misses on miss."""
57
- client = _get_client()
58
- if client is None:
59
- return None
60
- try:
61
- key = _cache_key(content)
62
- raw = await client.get(key)
63
- if raw is None:
64
- await client.incr("tco:stats:misses")
65
- return None
66
- await client.incr("tco:stats:hits")
67
- return json.loads(raw)
68
- except Exception:
69
- return None
70
-
71
-
72
- async def cache_set(content: str | bytes, parsed: dict) -> None:
73
- """Store parse result. Fire-and-forget — never blocks or raises."""
74
- client = _get_client()
75
- if client is None:
76
- return
77
- try:
78
- key = _cache_key(content)
79
- await client.set(key, json.dumps(parsed), ex=CACHE_TTL)
80
- except Exception:
81
- pass
82
-
83
-
84
- async def cache_stats() -> dict:
85
- """Return cache statistics. Returns zeros if Redis is not configured."""
86
- client = _get_client()
87
- configured = cachly_configured()
88
- if client is None:
89
- return {
90
- "configured": configured,
91
- "hits": 0,
92
- "misses": 0,
93
- "key_count": 0,
94
- "hit_rate": 0.0,
95
- "ttl_days": CACHE_TTL // 86400,
96
- }
97
- try:
98
- hits_raw, misses_raw = await client.mget("tco:stats:hits", "tco:stats:misses")
99
- hits = int(hits_raw or 0)
100
- misses = int(misses_raw or 0)
101
- total = hits + misses
102
- hit_rate = round(hits / total, 4) if total > 0 else 0.0
103
-
104
- # count tco:parse:* keys — SCAN is O(1) per call, safe on large keyspaces
105
- key_count = 0
106
- async for _ in client.scan_iter("tco:parse:*", count=100):
107
- key_count += 1
108
-
109
- return {
110
- "configured": True,
111
- "hits": hits,
112
- "misses": misses,
113
- "key_count": key_count,
114
- "hit_rate": hit_rate,
115
- "ttl_days": CACHE_TTL // 86400,
116
- }
117
- except Exception:
118
- return {
119
- "configured": configured,
120
- "hits": 0,
121
- "misses": 0,
122
- "key_count": 0,
123
- "hit_rate": 0.0,
124
- "ttl_days": CACHE_TTL // 86400,
125
- }
126
-
127
-
128
- def cachly_configured() -> bool:
129
- """True if CACHLY_REDIS_URL is set."""
130
- import os
131
- return bool(os.getenv("CACHLY_REDIS_URL", ""))