Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
292 changes: 292 additions & 0 deletions .opencode/plans/1770449315940-glowing-knight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
# Vendor Agreement Example - Python Flask + React

## Overview

Create a new example demonstrating a vendor agreement workflow where:
1. User fills out a web form with service agreement details
2. A PDF is generated on-the-fly from the form data using WeasyPrint
3. The PDF is uploaded to Documenso and a signing session is created
4. User is redirected to an embedded signing view to sign the agreement

**Tech Stack:**
- **Backend:** Python 3.11+ with Flask, managed by `uv`
- **Frontend:** React with Vite (dev proxy to Flask, production served by Flask)
- **PDF Generation:** WeasyPrint (HTML/CSS to PDF)
- **Linting/Formatting:** Ruff
- **Documenso:** Python SDK (`documenso_sdk`) + React Embed (`@documenso/embed-react`)

---

## Project Structure

```
vendor-agreement/
├── .env.example
├── .gitignore
├── .python-version
├── pyproject.toml
├── uv.lock
├── README.md
├── app/
│ ├── __init__.py
│ ├── main.py # Flask app entry point
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── api.py # API routes (form submission, document creation)
│ │ └── views.py # Serve React app
│ ├── services/
│ │ ├── __init__.py
│ │ ├── pdf_generator.py # WeasyPrint PDF generation
│ │ └── documenso.py # Documenso SDK wrapper
│ └── templates/
│ └── agreement.html # HTML template for PDF generation
├── frontend/
│ ├── package.json
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── index.html
│ └── src/
│ ├── main.tsx
│ ├── App.tsx
│ ├── components/
│ │ ├── AgreementForm.tsx # Multi-step form
│ │ └── SigningView.tsx # Documenso embed wrapper
│ └── lib/
│ └── api.ts # API client
└── static/ # Built React app (gitignored, created on build)
```

---

## Form Fields (Service Agreement Focused)

### Vendor Information
- Company Name
- Contact Name
- Email
- Phone
- Address (Street, City, State, ZIP)

### Service Agreement Details
- Service Description (textarea)
- Pricing / Rate (currency input)
- Payment Terms (dropdown: Net 15, Net 30, Net 60)
- Contract Start Date
- Contract Duration (dropdown: 3 months, 6 months, 1 year, 2 years)

---

## Implementation Steps

### 1. Initialize Python Project with UV

```bash
cd examples
mkdir vendor-agreement && cd vendor-agreement
uv init --python 3.11
uv add flask flask-cors weasyprint documenso-sdk python-dotenv
uv add --dev ruff
```

**Files to create:**
- `pyproject.toml` - Project config with Ruff settings
- `.python-version` - Pin Python version
- `.gitignore` - Python + Node ignores

### 2. Create Flask Application

**`app/main.py`:**
- Initialize Flask app
- Configure CORS for development
- Register blueprints (api, views)
- Serve static React build in production
- Load environment variables

**`app/routes/api.py`:**
- `POST /api/agreement` - Accept form data, generate PDF, create Documenso document, return signing token

**`app/routes/views.py`:**
- Serve React app for all non-API routes (SPA fallback)

### 3. PDF Generation Service

**`app/services/pdf_generator.py`:**
- Use WeasyPrint to convert HTML template to PDF
- Accept form data dict, render Jinja2 template, generate PDF bytes

**`app/templates/agreement.html`:**
- Professional agreement template with CSS styling
- Placeholders for all form fields
- Signature line at bottom
- Use CSS `@page` rules for proper PDF formatting

### 4. Documenso Integration Service

**`app/services/documenso.py`:**
- Initialize Documenso SDK with API key from environment
- `create_document(pdf_bytes, recipient_email, recipient_name)`:
1. Create document with SDK
2. Upload PDF
3. Add signature field at bottom of document
4. Add recipient
5. Distribute document
6. Return signing token

### 5. React Frontend Setup

```bash
cd frontend
npm create vite@latest . -- --template react-ts
npm install @documenso/embed-react
npm install -D tailwindcss @tailwindcss/vite
```

**`vite.config.ts`:**
- Configure proxy to Flask backend (`/api` -> `http://localhost:5000`)
- Build output to `../static`

**Components:**

**`AgreementForm.tsx`:**
- Multi-section form with all fields
- Form validation
- Submit handler that POSTs to `/api/agreement`
- Loading state during submission
- On success, transition to SigningView with token

**`SigningView.tsx`:**
- Wrapper around `EmbedSignDocument` from `@documenso/embed-react`
- Pass signing token from API response
- Handle `onDocumentCompleted` callback
- Show success message after signing

**`App.tsx`:**
- State management for form -> signing flow
- Conditional rendering of form vs signing view

### 6. Environment Configuration

**`.env.example`:**
```env
# Documenso Configuration
DOCUMENSO_API_KEY=your-api-key-here
DOCUMENSO_HOST=https://app.documenso.com

# Flask Configuration
FLASK_ENV=development
FLASK_DEBUG=1
```

### 7. Development Workflow

**Start backend:**
```bash
uv run flask --app app.main run --port 5000
```

**Start frontend (separate terminal):**
```bash
cd frontend && npm run dev
```

Frontend dev server runs on port 5173, proxies API requests to Flask on 5000.

### 8. Production Build

```bash
# Build React app
cd frontend && npm run build # outputs to ../static

# Run Flask (serves React from static/)
uv run flask --app app.main run
```

---

## Key Files to Create

| File | Purpose |
|------|---------|
| `pyproject.toml` | Python project config, dependencies, Ruff settings |
| `app/main.py` | Flask app initialization and configuration |
| `app/routes/api.py` | API endpoint for agreement creation |
| `app/routes/views.py` | Serve React SPA |
| `app/services/pdf_generator.py` | WeasyPrint HTML-to-PDF conversion |
| `app/services/documenso.py` | Documenso SDK integration |
| `app/templates/agreement.html` | PDF template with styling |
| `frontend/vite.config.ts` | Vite config with proxy and build settings |
| `frontend/src/App.tsx` | Main React app with form/signing flow |
| `frontend/src/components/AgreementForm.tsx` | Multi-field agreement form |
| `frontend/src/components/SigningView.tsx` | Documenso embed wrapper |
| `README.md` | Documentation and setup instructions |

---

## API Flow

```
1. User fills form in React app
2. POST /api/agreement with form data
3. Flask receives request
4. pdf_generator.py renders HTML template with form data
5. WeasyPrint converts HTML to PDF bytes
6. documenso.py creates document via SDK:
- Create document
- Upload PDF
- Add signature field
- Add recipient
- Distribute
7. Return { signingToken, documentId } to frontend
8. React shows EmbedSignDocument with token
9. User signs, onDocumentCompleted fires
10. Show success message
```

---

## Verification Steps

1. **Backend starts:** `uv run flask --app app.main run` - no errors
2. **Frontend builds:** `cd frontend && npm run build` - compiles successfully
3. **Form submission works:** Fill form, submit, get signing token response
4. **PDF generated correctly:** Check PDF contains all form data
5. **Signing works:** Can sign document in embedded view
6. **Completion callback:** Success message shows after signing
7. **Ruff passes:** `uv run ruff check .` - no linting errors

---

## Dependencies

### Python (pyproject.toml)
- `flask` - Web framework
- `flask-cors` - CORS handling for development
- `weasyprint` - HTML to PDF conversion
- `documenso-sdk` - Documenso API client
- `python-dotenv` - Environment variable loading
- `ruff` (dev) - Linting and formatting

### Node (frontend/package.json)
- `react`, `react-dom` - UI framework
- `@documenso/embed-react` - Signing embed component
- `typescript` - Type safety
- `vite` - Build tool
- `tailwindcss` - Styling

---

## Notes

- WeasyPrint requires system dependencies (Pango). Document in README with install instructions for macOS (`brew install pango`) and Linux (`apt install libpango-1.0-0`)
- The Documenso Python SDK is in beta (v0.4.0) but stable for document creation workflow
- Use `DOCUMENSO_HOST` env var to support both cloud and self-hosted instances
- React embed requires `NEXT_PUBLIC_DOCUMENSO_HOST` pattern, but since we're using Vite we'll use `VITE_DOCUMENSO_HOST`
6 changes: 6 additions & 0 deletions python-flask-vendor-agreement/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Documenso Configuration
DOCUMENSO_API_KEY=your-api-key-here
DOCUMENSO_HOST=https://app.documenso.com

# Flask Configuration
FLASK_DEBUG=1
36 changes: 36 additions & 0 deletions python-flask-vendor-agreement/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.venv/
venv/
ENV/
.eggs/
*.egg-info/
*.egg

# Environment
.env
.env.local

# IDE
.idea/
.vscode/
*.swp
*.swo

# Build artifacts
static/
dist/

# Logs
*.log

# Ruff cache
.ruff_cache/

# Node (frontend)
frontend/node_modules/
frontend/dist/
1 change: 1 addition & 0 deletions python-flask-vendor-agreement/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11
55 changes: 55 additions & 0 deletions python-flask-vendor-agreement/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.PHONY: install dev backend frontend build clean

# macOS: WeasyPrint needs Homebrew's library path for pango/glib
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
export DYLD_FALLBACK_LIBRARY_PATH := $(shell brew --prefix)/lib
endif

# Install all dependencies
install:
uv sync
cd frontend && npm install

# Run both backend and frontend in development mode
dev:
@echo "Starting backend on http://localhost:5001"
@echo "Starting frontend on http://localhost:5173"
@echo ""
@make -j2 backend frontend

# Run Flask backend (port 5001 to avoid macOS AirPlay conflict on 5000)
backend:
uv run flask --app app.main run --port 5001

# Run Vite frontend dev server
frontend:
cd frontend && npm run dev

# Build frontend for production
build:
cd frontend && npm run build

# Run production server (serves built frontend from Flask)
prod: build
uv run flask --app app.main run --port 5001

# Lint and format check
lint:
uv run ruff check .
uv run ruff format --check .
cd frontend && npm run lint
cd frontend && npm run format:check

# Format code
format:
uv run ruff format .
cd frontend && npm run format

# Clean build artifacts
clean:
rm -rf static/
rm -rf frontend/node_modules/
rm -rf .venv/
rm -rf __pycache__/
rm -rf .ruff_cache/
Loading