Perfect — thanks for pasting the article. This actually confirms (and strengthens) the architectural decisions we already sketched, and it lets us make a few very concrete, high-leverage choices for your platform.
Below is a clean synthesis + execution plan, written so you can: • copy sections as .txt • implement one file at a time • reuse your existing documents2 + trips + assistant stack • avoid dead ends or duplicated systems
I’ll be explicit about what stays, what is deprecated, and what is added.
⸻
🧠 KEY REALISATION (VERY IMPORTANT)
You now have three generations of the same idea: 1. Apps Script era Sheets → Docs → deterministic population 2. documents2 era TripBuilder → LineItems → Document sections 3. Tally + Next App Router era External structured input → internal mutation
👉 They are not competing systems. 👉 They are different input adapters into the same core pipeline.
Your mistake would be trying to merge them. Your strength is to normalize them.
⸻
🧩 FINAL CANONICAL PIPELINE (LOCK THIS IN)
[Tally | In-app Form | AI Assist | Sheet Import] ↓ Normalized Draft (TripDraftData) ↓ Pricing Engine ↓ LineItems ↓ documents2 (TripOffer template) ↓ Optional AI refinement
📌 Everything feeds TripDraftData 📌 Everything ends in documents2
This matches exactly how your Apps Script worked — just cleaner.
⸻
1️⃣ DECISION: TALLY INTEGRATION MODE (FINAL)
Based on the article and your needs, the correct mode for you is:
❌ NOT webhooks ❌ NOT cron jobs ✅ Client-side Tally.openPopup + onSubmit
Why this is perfect for your app: • You already have auth context • You already have user, project, space • You can attach entityId, projectId, spaceId • You can choose which document or trip to mutate
This is strictly superior to the webhook-only approach.
⸻
2️⃣ WHAT TALLY IS IN YOUR SYSTEM
Let’s name it clearly:
Tally = External Draft Input Adapter
It does not: • generate documents • apply pricing • create offers directly
It only: • collects structured answers • submits them into your app context • creates or updates a draft entity
⸻
3️⃣ NEW MODULE: tally/ (VERY SMALL)
You only need 3 files to start.
⸻
📁 app/lib/tally/types.ts
export type TallyFieldAnswer = { id: string; title: string; type: string; answer: { value?: string; values?: string[]; } | null; };
export type TallyPayload = { fields: TallyFieldAnswer[]; submissionId: string; };
⸻
📁 app/lib/tally/mapTallyToTripDraft.ts
This is the equivalent of your Apps Script _getRutaData().
import type { TripDraftData } from "@/app/(pages)/documents2/types/DocTripSection.types"; import type { TallyPayload } from "./types";
export function mapTallyToTripDraft(payload: TallyPayload): TripDraftData { const get = (label: string) => payload.fields.find(f => f.title === label)?.answer?.value ?? "";
return { id: crypto.randomUUID(), title: get("Trip title"), pax: Number(get("Participants") || 1), startDate: get("Start date"), endDate: get("End date"), style: get("Trip style") as any, cities: [], // populated later or via follow-up }; }
📌 This mirrors your Sheet-cell extraction logic.
⸻
📁 app/api/tally/route.ts
This becomes your single intake point.
import { NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/auth"; import { mapTallyToTripDraft } from "@/app/lib/tally/mapTallyToTripDraft"; import prisma from "lib/prisma";
export async function POST(req: Request) { const payload = await req.json(); const user = await getCurrentUser();
if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }
const tripDraft = mapTallyToTripDraft(payload);
const doc = await prisma.document.create({ data: { type: "trip-offer-v1", ownerId: user.id, data: { tripSection: { kind: "trip", tripData: tripDraft, }, }, }, });
return NextResponse.json({ ok: true, documentId: doc.id, }); }
📌 This is your Sheets → Doc replacement.
⸻
4️⃣ CLIENT-SIDE TALLY TRIGGER (GLOBAL OR CONTEXTUAL)
This matches the article but with entity awareness.
⸻
📁 components/tally/TallyTripPopup.tsx
"use client";
export default function TallyTripPopup({ projectId, spaceId, }: { projectId?: number; spaceId?: number; }) { const open = () => { // @ts-ignore window.Tally.openPopup("YOUR_FORM_ID", { layout: "modal", width: 600, overlay: true, hiddenFields: { projectId, spaceId, }, onSubmit: async (payload: any) => { await fetch("/api/tally", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); }, }); };
return ( <button onClick={open} className="btn"> 🧾 Create Trip from Form </button> ); }
📌 This is exactly where Tally shines for your users.
⸻
5️⃣ HOW THIS CONNECTS TO documents2 (CRITICAL)
When the document opens: • tripSection.tripData already exists • TripBuilder loads immediately • Pricing can be computed • LineItems inserted • Offer letter generated
This replaces 100% of: • Apps Script doc clearing • Marker logic • Manual copying
⸻
6️⃣ WHERE AI FITS (AND WHERE IT DOES NOT)
❌ AI should NOT: • parse Tally payloads • decide pricing • build line items
✅ AI SHOULD: • write the intro letter • rewrite itinerary paragraphs • explain options • generate variants
You already have everything needed for this via: • runDevLayerAction • personas • contexts • document sections
⸻
7️⃣ OPTIONAL: AI-ASSISTED FORM FILLING (FUTURE)
Later, you can add: • “✨ Fill this form for me” • AI pre-fills Tally hidden fields • Or fills in-app forms
But this is after your deterministic pipeline is stable.
⸻
8️⃣ YOUR MIGRATION IS 100% CONSISTENT
Let me say this clearly:
Your Apps Script logic was excellent. You are not abandoning it — you are porting it.
The mental model is unchanged: • structured input • deterministic generation • optional AI polish
You are now doing it: • with Prisma • with versioned documents • with reusable sections • with real product potential
⸻
🧾 WHAT YOU SHOULD SAVE AS .txt NOW 1. This entire answer 2. mapTallyToTripDraft.ts 3. trip-offer-v1 template 4. populateTripOfferFromTrip.ts 5. TallyTripPopup.tsx
That’s a 2–3 week roadmap, clearly scoped.
When you’re ready, next good prompts would be: • “Generate full trip-offer-v1 template + registry entry” • “Generate AI-in-document assistant section” • “Generate Space builder cloned from Project”