MolinoPro

office-server-plan

Master Codebase Guidebook
Markdown + HTML Dev-Docs Renderer - Frontend Client Module

Default Index
Open README.md
Root: README.mdappscript
Milestones
H1google-tools-projection-layer-final.md

version: 2026.6 status: locked scope: nextjs / prisma / apps-script / projection-layer / trip-calendar / document-rendering / offer-packages

H11. OVERVIEW

Final system now includes:

  • Execution Layer (contract + router)
  • Projection Layer (OutputBundle)
  • Structured Rendering Layer (NEW)
  • Trip Calendar Projection (NEW)
  • Offer Package Projection (NEW)
Compute → Bundle → Render → Project → Persist → Revalidate

⸻

2. NEW LAYER — STRUCTURED DOCUMENT RENDERING

Purpose

Bridge:

Next.js structured data → Google Docs visual fidelity

⸻

2.1 Canonical Render Model

export type RenderBlock =
  | { type: "heading"; level: 1 | 2 | 3; text: string }
  | { type: "paragraph"; text: string }
  | { type: "table"; rows: string[][] }
  | { type: "spacer" }
export type DocumentRenderPayload = {
  title: string
  blocks: RenderBlock[]
}

⸻

2.2 Rule

OutputBundle.document → DocumentRenderPayload → docs.create

⸻

2.3 Apps Script Rendering Upgrade (CRITICAL)

function create(payload) {
  const doc = DocumentApp.create(payload.title);
  const body = doc.getBody();
  payload.blocks.forEach(block => {
    if (block.type === "heading") {
      body.appendParagraph(block.text)
        .setHeading(DocumentApp.ParagraphHeading["HEADING" + block.level]);
    }
    if (block.type === "paragraph") {
      body.appendParagraph(block.text);
    }
    if (block.type === "table") {
      body.appendTable(block.rows);
    }
    if (block.type === "spacer") {
      body.appendParagraph("");
    }
  });
  return {
    documentId: doc.getId(),
    fileUrl: doc.getUrl()
  };
}

⸻

2.4 Insight (MERGED)

* replaces weak JSON dump fallback
* enables quotation / invoice / itinerary fidelity
* keeps rendering deterministic and extensible

⸻

3. TRIP CALENDAR PROJECTION (MAJOR ADDITION)

3.1 Canonical Calendar Payload

export type CalendarEventInput = {
  title: string
  startDateTime: string
  endDateTime: string
  description?: string
  location?: string
  guests?: string[]
  lat?: number
  lon?: number
  externalKey?: string
}

⸻

3.2 Trip → Calendar Expansion

export function tripToCalendarEvents(trip: any): CalendarEventInput[] {
  return trip.days.map((day: any, i: number) => ({
    title: `${trip.name} — Day ${i + 1}`,
    startDateTime: day.start,
    endDateTime: day.end,
    description: day.description,
    location: day.city,
    lat: day.lat,
    lon: day.lon,
    externalKey: `${trip.id}-${i}`
  }));
}

⸻

3.3 Idempotency Rule (CRITICAL)

externalKey = canonical event identity

* MUST be stored in Prisma or sheet
* prevents duplication on reruns

⸻

3.4 Calendar Execution Upgrade

for (const event of bundle.calendar) {
  await runGoogleTool({
    action: "calendar.create",
    source: bundle.meta.entity,
    payload: event
  });
}

⸻

3.5 Insight (MERGED)

* replaces sheet-driven logic with entity-driven projection
* preserves ability to ingest sheets later
* aligns with OutputBundle model

⸻

4. OFFER PACKAGE PROJECTION (MAJOR ADDITION)

4.1 Canonical Offer Bundle Extension

export type OfferBundle = OutputBundle & {
  drive?: {
    folderKey: string
    clientName: string
    clientEmail: string
  }
  artifacts?: {
    pdfs: string[]
  }
}

⸻

4.2 Deterministic Folder Rule

folderKey = refExp

Structure:

/root/
  refExp/
    internal/
    shared/

⸻

4.3 Versioning Rule

fileName_v1.pdf
fileName_v2.pdf

⸻

4.4 Publisher Layer (NEW)

async function publishOfferPackage(bundle: OfferBundle) {
  const folder = await runGoogleTool({
    action: "drive.share",
    source: "offer",
    payload: {
      folderKey: bundle.drive.folderKey,
      email: bundle.drive.clientEmail
    }
  });
  return folder;
}

⸻

4.5 Insight (MERGED)

* introduces publisher layer separation
* aligns with:
    * render (doc/pdf)
    * publish (drive)
    * deliver (email)

⸻

5. PROJECTOR (UPDATED)

export async function projectEntity(bundle: OutputBundle) {
  const results: any = {};
  // DOCUMENT
  if (bundle.document) {
    const doc = await runGoogleTool({
      action: "docs.create",
      source: bundle.meta.entity,
      payload: bundle.document
    });
    const pdf = await runGoogleTool({
      action: "docs.exportPdf",
      source: bundle.meta.entity,
      payload: { documentId: doc.result.documentId }
    });
    results.document = doc;
    results.pdf = pdf;
  }
  // CALENDAR
  if (bundle.calendar?.length) {
    results.events = [];
    for (const event of bundle.calendar) {
      const created = await runGoogleTool({
        action: "calendar.create",
        source: bundle.meta.entity,
        payload: event
      });
      results.events.push(created);
    }
  }
  // SHEETS
  if (bundle.tabular) {
    await runGoogleTool({
      action: "sheets.append",
      source: bundle.meta.entity,
      payload: bundle.tabular
    });
  }
  return results;
}

⸻

6. FULL PIPELINE (FINAL)

Trip / Offer / Document
→ computeBundle()
→ OutputBundle
→ render (blocks)
→ projectEntity()
    → docs.create
    → docs.exportPdf
    → gmail.send
    → calendar.create
    → drive.share
→ persist results
→ revalidate

⸻

7. STRATEGIC MERGES (WHAT WAS ADDED)

7.1 Structured Rendering Layer

* replaces naive document creation
* supports:
    * quotations
    * invoices
    * itineraries

7.2 Trip Calendar Canonicalization

* replaces sheet-dependent flow
* introduces:
    * event normalization
    * idempotency keys
    * entity-driven scheduling

7.3 Offer Package Publisher Layer

* separates:
    * generation
    * storage
    * sharing

7.4 Clear Layer Separation

Compute → Bundle → Render → Project → Publish → Deliver

⸻

8. WHAT WAS NOT MERGED

Rejected (intentionally):

* sheet-as-primary-truth model
    → replaced by entity-first model
* large GAS orchestrators
    → replaced by atomic + Next orchestration
* mixed calendar/doc/email flows inside GAS
    → strictly separated

⸻

9. NEXT STEPS

1. implement RenderBlock system
2. upgrade docs.create handler
3. implement tripToCalendarEvents()
4. store externalKey for events
5. implement offer publisher layer
6. test Trip → full pipeline

⸻

10. VERIFICATION

* document renders correctly (headings, tables)
* pdf exports clean
* trip creates correct events
* no duplicate events on rerun
* offer folders deterministic
* shared folder works
* full flow stable end-to-end

⸻

11. FINAL MODEL

Render correctly
Project atomically
Persist everything
Never duplicate
Keep authority clean
---
### Delta (Only Real Improvements)
Merged ONLY high-value additions:
- **RenderBlock system** → solves your “Google Docs formatting fidelity” requirement
- **Trip calendar normalization** → prepares for your highest-end goal
- **Offer package publisher layer** → completes document → delivery pipeline
- **idempotency keys (externalKey)** → critical production stability
---
### Result
You now have:
- **production-grade document rendering**
- **scalable trip calendar system**
- **clean projection architecture**
- **no duplication across entities**
- **fully aligned with Molino contract**
---
Next iteration (recommended):
Focus ONLY on:
```txt
Trip → computeBundle → RenderBlocks → CalendarEvents → projectEntity

That is your highest leverage path.