This is a Next.js app for selling paid Lithuanian land/forest valuation requests. Users create an account, submit cadastral/location/notes/photos, pay through Stripe Checkout, and then an expert completes the valuation from Telegram.
The current production-like workflow is human-in-the-loop:
- User submits a valuation request.
- User pays via Stripe Checkout.
- Stripe sends
checkout.session.completedto the app. - The app marks the request as
PROCESSING. - The app sends the full request and uploaded photos to a configured Telegram chat.
- An expert replies to the original Telegram request message with a structured template.
- The Telegram webhook parses the reply, creates the report, and marks the request
COMPLETED. - The client page polls and updates automatically when the report is ready.
- Next.js 15 App Router
- React 18
- TypeScript
- Tailwind CSS
- Prisma ORM
- PostgreSQL, currently Neon in the existing setup
- Stripe Checkout for one-time payments
- Telegram Bot API for expert review and sell-request notifications
- Vitest for tests
ValuationRequest.status uses the existing enum:
PENDING_PAYMENT: request exists, user has not completed Stripe checkout.PROCESSING: payment succeeded and request has been sent to Telegram, or is waiting for Telegram completion.COMPLETED: a valid expert reply was parsed and aValuationReportexists.FAILED: payment webhook processing failed, usually Telegram delivery/config failure.
The expert must reply directly to the original bot request message with:
VALUE_EUR:
LAND_AREA_SQM:
FOREST_SHARE_PCT:
SUMMARY:
DETAILS:
Example:
VALUE_EUR:19000
LAND_AREA_SQM:10000
FOREST_SHARE_PCT:50%
SUMMARY:Orientacinė sklypo vertė yra 19 000 EUR.
DETAILS:Vertinimas atliktas pagal pateiktus duomenis, vietovės aprašymą ir nuotraukas.
FOREST_SHARE_PCT may include %. The app accepts 50 and 50%.
The bot replies in Telegram after processing:
- success: confirms the request ID and saved values.
- invalid format: explains which required fields are wrong.
- not a reply: tells the expert to reply to the original request message.
- unmatched reply: tells the expert the reply did not match an active request.
Create .env from .env.example.
Required:
DATABASE_PROVIDER=postgresql
DATABASE_URL="postgresql://..."
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_ID=price_...
NEXT_PUBLIC_BASE_URL=https://your-public-url
TELEGRAM_BOT_TOKEN=123456789:bot-token
TELEGRAM_CHAT_ID=123456789
TELEGRAM_WEBHOOK_SECRET=random-secret-stringOptional legacy/debug values:
OPENROUTER_API_KEY=sk-or-v1-...
OPENROUTER_MODEL=openai/gpt-4.1-mini
OPENROUTER_VISION_MODEL=openai/gpt-4.1-mini
OPENROUTER_PRICING_MODEL=openai/gpt-4.1-mini
DEBUG_VALUATION=true
DEBUG_TELEGRAM=trueOpenRouter code still exists in lib/valuation.ts for tests/legacy fallback,
but paid valuation requests now go to Telegram, not OpenRouter.
This must be a public HTTPS URL when Telegram needs to call the app.
For the current local setup, this is usually an ngrok URL, for example:
NEXT_PUBLIC_BASE_URL=https://daunting-plasma-gravity.ngrok-free.devAvoid a trailing slash.
Good:
NEXT_PUBLIC_BASE_URL=https://example.ngrok-free.devAvoid:
NEXT_PUBLIC_BASE_URL=https://example.ngrok-free.dev/Install dependencies:
npm installGenerate Prisma client:
npm run prisma:generateRun the app:
npm run devThe site runs at:
http://localhost:3000
Keep this terminal running.
In a separate terminal, run:
stripe listen --forward-to localhost:3000/api/stripe/webhookStripe CLI prints a webhook signing secret like:
whsec_...
Put that value in .env:
STRIPE_WEBHOOK_SECRET=whsec_...Then restart npm run dev so Next reads the updated env.
The app only handles:
checkout.session.completed
Telegram cannot call localhost, so expose the local app with ngrok.
Example:
ngrok http 3000Copy the HTTPS forwarding URL and set it in .env:
NEXT_PUBLIC_BASE_URL=https://your-current-ngrok-url.ngrok-free.appRestart npm run dev.
Then register the Telegram webhook:
$token = "YOUR_TELEGRAM_BOT_TOKEN"
$secret = "YOUR_TELEGRAM_WEBHOOK_SECRET"
$baseUrl = "https://your-current-ngrok-url.ngrok-free.app"
Invoke-RestMethod "https://api.telegram.org/bot$token/setWebhook?url=$baseUrl/api/telegram/webhook&secret_token=$secret"Check what Telegram currently has saved:
Invoke-RestMethod "https://api.telegram.org/bot$token/getWebhookInfo"If you use free/random ngrok, the URL changes often. Every time it changes:
- Update
NEXT_PUBLIC_BASE_URLin.env. - Restart
npm run dev. - Run
setWebhookagain with the new URL. - Verify with
getWebhookInfo.
If Telegram says:
404 Not Found
it is probably pointing at an old/wrong URL.
If Telegram says:
502 Bad Gateway
ngrok is reachable but the local Next server is probably not running.
Best long-term fix: use a reserved/static ngrok domain or deploy to a real HTTPS domain.
The app uses Prisma with PostgreSQL.
Schema file:
prisma/schema.prisma
Current important models:
UserSessionValuationRequestValuationReport
Telegram tracking fields live on ValuationRequest:
telegramReviewChatId
telegramReviewMessageId
telegramReviewSentAt
telegramReviewCompletedAtThe current Neon DB was not fully created from Prisma migration history.
Because of that, prisma migrate dev may fail with a shadow database error like:
P3006
The underlying table for model `ValuationRequest` does not exist.
For the Telegram tracking migration, the working command was:
npx prisma db execute --schema prisma/schema.prisma --file prisma/migrations/20260422120000_add_telegram_review_tracking/migration.sql
npm run prisma:generateVerify the columns:
npx prisma db pull --print | Select-String "telegramReview"If prisma:generate fails on Windows with EPERM rename query_engine...,
some Node process is locking Prisma's DLL. Stop Node processes and rerun:
Get-Process node -ErrorAction SilentlyContinue | Stop-Process
npm run prisma:generateFor a clean future production DB that does have migration history, use:
npx prisma migrate deployRun these three things:
Terminal 1:
npm run devTerminal 2:
stripe listen --forward-to localhost:3000/api/stripe/webhookTerminal 3:
ngrok http 3000Then:
- Update
.envwith ngrokNEXT_PUBLIC_BASE_URL. - Restart
npm run dev. - Register Telegram webhook using
setWebhook. - Open
http://localhost:3000. - Register or log in.
- Create a valuation request.
- Pay through Stripe Checkout test mode.
- Confirm the bot posts the request to Telegram.
- Reply to the original Telegram request message with the required template.
- Confirm the bot replies
Evaluation saved. - Return to the app; checkout success or valuation detail page should update to
COMPLETED.
app/
api/
auth/ Login/register/logout API routes
stripe/webhook/ Stripe payment webhook
telegram/webhook/ Telegram expert reply webhook
valuation/ Valuation CRUD, lookup, sell-request routes
auth/ Login/register pages
checkout/ Stripe success/cancel pages
dashboard/ User dashboard
valuation/ New valuation and detail pages
lib/
auth.ts Session and auth helpers
i18n.ts Lithuanian/English/Russian UI strings
prisma.ts Prisma singleton
stripe.ts Stripe client
telegram.ts Telegram Bot API helpers
telegram-evaluation.ts Telegram request builder and reply parser
valuation.ts Legacy deterministic/OpenRouter valuation helpers
prisma/
schema.prisma Database schema
migrations/ SQL migrations
public/uploads/ Local uploaded valuation photos, gitignored
Stripe payment completion:
app/api/stripe/webhook/route.ts
Telegram reply processing:
app/api/telegram/webhook/route.ts
Telegram message format and parser:
lib/telegram-evaluation.ts
Telegram low-level API calls:
lib/telegram.ts
Valuation page polling:
app/valuation/[id]/ValuationClient.tsx
app/checkout/success/CheckoutSuccessClient.tsx
Sell-this-land Telegram notification:
app/api/valuation/[id]/sell-request/route.ts
Run all tests:
npm testRun production build/type checks:
npm run buildCurrent important test files:
lib/telegram-evaluation.test.ts
app/api/telegram/webhook/route.test.ts
app/api/stripe/webhook/route.test.ts
app/api/valuation/[id]/sell-request/route.test.ts
app/api/valuation/create/route.test.ts
lib/valuation.test.ts
Check Telegram webhook:
Invoke-RestMethod "https://api.telegram.org/bot$token/getWebhookInfo"Common causes:
- Webhook URL points to an old ngrok URL.
npm run devis not running.- ngrok is not running.
- Expert did not reply to the original bot request message.
TELEGRAM_CHAT_IDdoes not match the chat where replies happen.TELEGRAM_WEBHOOK_SECRETwas changed locally but webhook was not re-registered.
The expert likely replied to a photo message or copied text into a new message. They must reply directly to the original request message that contains the template.
Check:
stripe listen --forward-to localhost:3000/api/stripe/webhookis running.STRIPE_WEBHOOK_SECRETin.envmatches the current Stripe CLI secret.TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_IDare valid.- App terminal has no Stripe webhook errors.
ngrok can reach your machine, but Next is probably not running. Start:
npm run devTelegram is pointing at the wrong URL. Register webhook again with the current ngrok URL.
- User passwords are hashed with bcrypt.
- Session cookies are HTTP-only.
- Stripe webhook signatures are verified.
- Telegram webhook uses
TELEGRAM_WEBHOOK_SECRETvia Telegram'ssecret_tokenheader. - Protected valuation routes check ownership.
- Uploaded photos are stored under
public/uploads, which is gitignored.
For a proper deployment:
- Use a stable HTTPS domain.
- Set
NEXT_PUBLIC_BASE_URLto that domain. - Set Stripe webhook endpoint to:
https://your-domain.com/api/stripe/webhook
- Set Telegram webhook once:
https://your-domain.com/api/telegram/webhook
- Use production Stripe keys and production
STRIPE_WEBHOOK_SECRET. - Run database migrations against the production DB.
- Run:
npm run build
npm startPrivate project. All rights reserved.