ACT Phase I - Ordering System
A complete quote-to-project management system with automated Stripe quotes, e-signatures, and payment processing. Built with Python Flask and Firebase, integrated with ACT's existing project management workflow.
> Note: This system uses Stripe for all quote generation and payment processing.
Features
Technology Stack
Workflow
Stripe Quote Created โ Sent for Signature โ Signed โ Accepted โ
Payment โ In Progress โ CompletedDetailed Status Flow:
1. requested - Customer submits quote request 2. quote_priced - Admin sets pricing 3. estimate_sent / sent - Stripe quote created and sent for signature 4. accepted / approved - Customer signs via Dropbox Sign (auto-updates status) 5. in_progress - Payment received, work begins 6. completed - Project finished 7. cancelled / declined - Cancelled or declined at any stageSignature & Payment Flow:
Invoice Automation:
Prerequisites
Quick Start
1. Firebase Setup
Already configured for project `act-phase-i`:
2. Stripe Setup
1. Sign up at Stripe 2. Get your API keys from the dashboard (Developers โ API keys) 3. Enable Payment Links and Payment Methods in your Stripe dashboard 4. Update `.env` file with your secret and publishable keys
3. Dropbox Sign Setup
1. Sign up at Dropbox Sign 2. Go to API Settings โ Create an API key 3. (Optional) Create a template for proposals with custom fields 4. Configure webhook URL: `https://your-domain.com/api/dropbox-sign/webhook` 5. Update `.env` file
4. Environment Configuration
The `.env` file configuration:
# Firebase
FIREBASE_PROJECT_ID=act-phase-i
FIREBASE_CREDENTIALS_PATH=./service-account-key.json/act-phase-i-6644998ca560.jsonStripe Configuration (Quotes & Payments)
STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key
STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_keyDropbox Sign Configuration
DROPBOX_SIGN_API_KEY=your_dropbox_sign_api_key
DROPBOX_SIGN_CLIENT_ID=your_client_id # Optional
DROPBOX_SIGN_TEST_MODE=true # Set to false for production
DROPBOX_SIGN_TEMPLATE_ID=your_template_id # Optional - uses template if providedApplication
APP_URL=https://your-domain.com
EMAIL_SENDING_ENABLED=true
ORDER_CC_EMAIL=karenf@act.earth6
4. Install and Run
# Install dependencies
pip install -r requirements.txtRun the application
python3 app.pyAccess the application:
API Endpoints
Authentication
Quotes (Primary Admin Workflow)
Quote Management (Customer Portal)
Orders
Estimates
Invoices
Customer Portal Endpoints
Payments (Stripe)
Clients
Webhooks
---
Quote Pricing API (iOS App)
This is the only endpoint needed to price a project and automatically generate + send a Stripe quote with e-signature.
Overview
When you set a price, the system automatically: 1. โ Saves the price to the order in Firestore 2. โ Creates a professional Stripe quote with all project details 3. โ Sends the quote to the client via Dropbox Sign for e-signature 4. โ Updates project status to "proposal pending"
Endpoint
POST /api/quotes/by-project//price Authentication
Request Body
Minimal (most common):
{
"price": 1500
}With Optional Fields:
{
"price": 1500,
"notes": "Includes soil sampling and analysis",
"valid_days": 14
}Request Parameters
| Field | Type | Required | Description | |-------|------|----------|-------------| | `price` | number | Yes | Total price for the project in USD | | `notes` | string | No | Internal pricing notes (not shown to client) | | `valid_days` | number | No | Quote validity in days. Only used if order doesn't have `turnaround_days` set. Default: 14 days |
Success Response (200)
{
"success": true,
"order_id": "abc123",
"proposal_id": "PROP-A1B2C3D4",
"ACTProjectNumber": "11160",
"status": "proposal pending",
"stripe_quote_id": "qt_1234567890",
"stripe_quote_pdf_url": "https://files.stripe.com/v1/...",
"signature_request_id": "fa123abc456",
"message": "Quote created and sent successfully"
}Response Fields
| Field | Type | Description | |-------|------|-------------| | `success` | boolean | Always `true` for successful requests | | `order_id` | string | Firestore order document ID | | `proposal_id` | string | Unique proposal ID (e.g., "PROP-A1B2C3D4") - Store this to modify the proposal later | | `ACTProjectNumber` | string | ACT project number (e.g., "11160") | | `status` | string | New project status: `"proposal pending"` | | `stripe_quote_id` | string | Stripe quote ID for tracking | | `stripe_quote_pdf_url` | string | URL to download the quote PDF | | `signature_request_id` | string | Dropbox Sign signature request ID (null if signature sending failed) | | `message` | string | Success message |
Error Responses
404 - Project Not Found:
{
"error": "Order not found for ACTProjectNumber"
}400 - Missing Client:
{
"error": "Client not found for this order"
}400 - Stripe Not Configured:
{
"error": "Stripe not configured"
}400 - Missing Price:
{
"error": "Items are required for pricing"
}401 - Unauthorized:
{
"error": "Unauthorized"
}403 - Not Admin:
{
"error": "Admin access required"
}Example: iOS Swift Request
func sendQuote(projectNumber: String, price: Double) async throws {
let url = URL(string: "https://ordering.act.earth/api/quotes/by-project/\(projectNumber)/price")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = ["price": price]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw QuoteError.invalidResponse
}
if httpResponse.statusCode == 200 {
let result = try JSONDecoder().decode(QuoteResponse.self, from: data)
print("โ
Quote sent! Status: \(result.status)")
} else {
let error = try JSONDecoder().decode(ErrorResponse.self, from: data)
throw QuoteError.apiError(error.error)
}
}struct QuoteResponse: Codable {
let success: Bool
let orderId: String
let proposalId: String // Store this to update proposal later!
let ACTProjectNumber: String
let status: String
let stripeQuoteId: String
let stripeQuotePdfUrl: String?
let signatureRequestId: String?
let message: String
enum CodingKeys: String, CodingKey {
case success
case orderId = "order_id"
case proposalId = "proposal_id"
case ACTProjectNumber
case status
case stripeQuoteId = "stripe_quote_id"
case stripeQuotePdfUrl = "stripe_quote_pdf_url"
case signatureRequestId = "signature_request_id"
case message
}
}
struct ErrorResponse: Codable {
let error: String
}
Example: cURL Request
curl -X POST https://ordering.act.earth/api/quotes/by-project/11160/price \
-H "Authorization: Bearer YOUR_FIREBASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"price": 1500}'What Happens After Sending
1. System automatically generates site diagram (if ACT project number exists) - Calls Phase I API to generate site diagram for the project - Downloads PNG site diagram and converts it to PDF - Merges site diagram with quote PDF into single document 2. Client receives email from Dropbox Sign with the complete quote PDF (with site diagram) to review and sign 3. When client signs: - Order status automatically updates to `"accepted"` - If payment terms = "upon authorization", invoice is auto-created and sent - Project status changes to `"active"` (ready for work) 4. Client can view and pay the invoice from the customer portal
Important Notes
---
Modifying Existing Proposals
If you need to change the price or client for a proposal that was already created, use the update endpoint instead of creating a new one:
Update Proposal Endpoint
PUT /api/quotes/proposals/Authentication: Admin Firebase ID token required
Request Body:
{
"price": 2000,
"client_id": "different_client_firebase_uid"
}What it does:
Response (200):
{
"success": true,
"proposal_id": "PROP-A1B2C3D4",
"order_id": "abc123",
"stripe_quote_id": "qt_new_quote_id",
"message": "Proposal updated successfully"
}Usage Pattern: 1. Create initial proposal: `POST /api/quotes/by-project/11160/price` 2. Store the returned `proposal_id` 3. If price/client changes: `PUT /api/quotes/proposals/PROP-A1B2C3D4` (creates new draft quote) 4. Review the updated quote, then send: `POST /api/quotes/proposals/PROP-A1B2C3D4/send` 5. Never call the pricing endpoint twice for the same project unless you want a separate proposal
---
Sending/Resending Proposals
If you need to resend a proposal for signature (e.g., after making manual changes in Stripe dashboard, or if the signature request expired):
Send Proposal Endpoint
POST /api/quotes/proposals//send Authentication: Admin Firebase ID token required
Request Body: None (empty POST)
What it does:
Response (200):
{
"success": true,
"proposal_id": "PROP-A1B2C3D4",
"order_id": "abc123",
"stripe_quote_id": "qt_1234567890",
"signature_request_id": "fa_new_signature_id",
"message": "Proposal sent for signature"
}Use cases:
---
Renewing Quotes
If a quote has expired or you need to create a completely new quote for the same project:
Renew Quote Endpoint
POST /api/quotes//stripe/quote/renew Authentication: Admin Firebase ID token required
Request Body (optional):
{
"valid_days": 30
}What it does:
Response (200):
{
"success": true,
"old_quote_id": "qt_old123...",
"new_quote_id": "qt_new456...",
"stripe_quote_pdf_url": "https://files.stripe.com/...",
"status": "draft",
"message": "Old quote cancelled and new draft quote created"
}Use cases:
Firebase Collections
`clients`
{
client_id: "firebase_uid",
name: "John Doe",
email: "john@example.com",
phone: "+1234567890",
company: "ACME Corp",
address: "123 Main St",
payment_terms: "upon authorization", // Default payment terms for quotes
created_at: Timestamp,
stripe_customer_id: "cus_stripe_id"
}`orders`
{
order_id: "uuid",
ACTProjectNumber: "10891",
client_id: "firebase_uid",
client_name: "John Doe",
address: "Project address",
tax_parcels: ["Block 123 Lot 45", "Block 123 Lot 46"],
customer_project_number: "CLIENT-2024-001", // Optional customer reference
status: "requested",
payment_status: "not_required",
estimate_id: "uuid",
invoice_id: "uuid",
items: [{description, quantity, unit_price}],
// Stripe Quote fields
stripe_quote_id: "qt_stripe_id",
stripe_quote_pdf_url: "https://...",
stripe_quote_status: "open",
stripe_quote_created_at: Timestamp,
// Stripe Invoice fields (auto-populated when quote accepted)
stripe_invoice_id: "in_stripe_id",
stripe_invoice_url: "https://...", // Hosted payment page URL
stripe_invoice_status: "open",
stripe_invoice_created_at: Timestamp,
stripe_invoice_sent_at: Timestamp,
created_at: Timestamp
}`proposals`
{
proposal_id: "PROP-123",
order_id: "uuid",
client_id: "firebase_uid",
amount: 2500.00,
status: "sent", // sent, accepted, approved, declined
accepted: false,
accepted_at: Timestamp,
approved: false, // For backward compatibility
approved_at: Timestamp,
signature_request_id: "dropbox_sign_request_id",
signature_status: "pending", // pending, viewed, signed, declined
signature_sent_at: Timestamp,
signature_completed_at: Timestamp,
stripe_quote_id: "qt_stripe_id",
created_at: Timestamp
}`projectManagement` (Existing Collection - Integrated)
{
ACTProjectNumber: "10891", // Document ID
order_id: "uuid", // Link to orders
clientName: "John Doe",
address: "Project address",
status: "requested",
createdBy: "user@email.com",
createdAt: Timestamp,
statusUpdatedAt: Timestamp,
proposal: {},
database: {},
inspection: {},
historicals: {},
adjacentLots: {},
report: {},
files: []
}Deployment to Cloud Run
Using Deployment Script
chmod +x deploy.sh
./deploy.shManual Deployment
# Build and push image
gcloud builds submit --tag gcr.io/act-phase-i/ordering-systemDeploy to Cloud Run
gcloud run deploy ordering-system \
--image gcr.io/act-phase-i/ordering-system \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars FIREBASE_PROJECT_ID=act-phase-i,STRIPE_SECRET_KEY=your-stripe-keyProject Structure
Ordering_system/
โโโ app.py # Main Flask application
โโโ config.py # Configuration management
โโโ requirements.txt # Python dependencies
โโโ Dockerfile # Container configuration
โโโ .env # Environment variables
โโโ services/ # Business logic services
โ โโโ firebase_service.py # Firebase Auth & Firestore
โ โโโ order_service.py # Order & project management
โ โโโ client_service.py # Client management
โ โโโ estimate_service.py # PDF estimate generation
โ โโโ invoice_service.py # PDF invoice generation
โ โโโ stripe_service.py # Stripe quote & payment processing
โ โโโ dropbox_sign_service.py # Dropbox Sign e-signatures
โ โโโ payment_service.py
โ โโโ braintree_service.py
โโโ utils/ # Utility modules
โ โโโ pdf_utils.py # PDF merging & image-to-PDF conversion
โโโ routes/ # API route handlers
โ โโโ auth_routes.py # Authentication endpoints
โ โโโ quote_routes.py # Quote request endpoints
โ โโโ order_routes.py # Order management endpoints
โ โโโ estimate_routes.py # Estimate endpoints
โ โโโ invoice_routes.py # Invoice endpoints
โ โโโ client_routes.py # Client management endpoints
โ โโโ dropbox_sign_routes.py # Dropbox Sign webhook endpoints
โ โโโ payment_routes.py
โ โโโ braintree_routes.py
โโโ middleware/ # Authentication middleware
โ โโโ auth_middleware.py # JWT token verification
โโโ static/ # Frontend files
โ โโโ index.html # Customer portal
โโโ generated_pdfs/ # Generated PDF filesKey Integrations
Stripe Quote & Payment System
Dropbox Sign Integration
Payment Terms Management
Stripe Payment Processing
Invoice Automation
Customer Portal
Security
Testing
Test Firebase Connection
python3 test_firebase.pyTest Stripe Quote & Dropbox Sign Integration
python3 test_stripe_quote_signature.pyReview Project Schema
python3 review_schema.pyTroubleshooting
Permission Errors
If you get `403 Missing or insufficient permissions`: 1. Go to https://console.cloud.google.com/iam-admin/iam?project=act-phase-i 2. Find service account: `firebase-adminsdk-fbsvc@act-phase-i.iam.gserviceaccount.com` 3. Add roles: Cloud Datastore User and Firebase AdminImport Errors
Make sure all dependencies are installed:pip install -r requirements.txtDropbox Sign Webhook Issues
Site Diagram Not Merging
If site diagrams aren't appearing in proposals:Stripe Quote Not Creating
Documentation
Detailed integration guides available:
Support
For issues and questions, contact the development team or open an issue in the repository.
License
MIT License