Deploy on DigitalOcean App Platform (GitHub-source build) + consolidate audit-fixes

Deploy config:
- .do/app.yaml: build the Dockerfile directly from GitHub (deploy_on_push) instead
  of a pre-built DOCR image; NEXT_PUBLIC_* set RUN_AND_BUILD_TIME with the
  propertymanagement.network domain so they bake into the client bundle; add
  custom domains block (apex + www); wire Sentry DSN (server + browser).

Included pending work from the audit-fixes branch:
- AI provider abstraction (OpenAI/Anthropic, admin-selectable; Anthropic default)
- Per-landlord e-signature (DocuSign OAuth + Dropbox Sign) + migration 0010
- Outbound webhooks / Zapier integration
- PayPal removal (Stripe-only billing)
- Storage hardening (fail-loud when Spaces unconfigured), security fixes

Verified: full production Docker build (same build-args as DO) passes clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leon Serfaty
2026-07-03 04:45:24 -04:00
co-authored by Claude Opus 4.8
parent 917a06ee85
commit 5495b94924
86 changed files with 7647 additions and 1182 deletions
-102
View File
@@ -1,102 +0,0 @@
// One-time PayPal setup: creates the product + the recurring billing plans and
// prints the plan IDs to paste into your environment.
//
// 1. Set PAYPAL_CLIENT_ID / PAYPAL_SECRET (and PAYPAL_ENVIRONMENT) in .env.local
// 2. node scripts/paypal-setup-plans.mjs
// 3. Copy the printed PAYPAL_*_PLAN_ID lines into .env.local / production env
//
// Amounts mirror the app's pricing (Pro $29/mo, Landlord $59/mo); yearly is
// billed at 10× monthly (~2 months free). Adjust in the PayPal dashboard if you
// want different annual pricing. Safe to re-run (it creates fresh plans).
import { config } from "dotenv"
config({ path: ".env.local", quiet: true })
const ENV = process.env.PAYPAL_ENVIRONMENT === "live" ? "live" : "sandbox"
const BASE = ENV === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"
const id = process.env.PAYPAL_CLIENT_ID
const secret = process.env.PAYPAL_SECRET
if (!id || !secret) {
console.error("[paypal-setup] Set PAYPAL_CLIENT_ID and PAYPAL_SECRET in .env.local first.")
process.exit(1)
}
async function getToken() {
const r = await fetch(`${BASE}/v1/oauth2/token`, {
method: "POST",
headers: {
Authorization: `Basic ${Buffer.from(`${id}:${secret}`).toString("base64")}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: "grant_type=client_credentials",
})
if (!r.ok) throw new Error(`auth ${r.status}: ${await r.text()}`)
return (await r.json()).access_token
}
const token = await getToken()
const post = (path, body) =>
fetch(`${BASE}${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
})
console.log(`[paypal-setup] Environment: ${ENV}`)
const prodRes = await post("/v1/catalogs/products", {
name: "Property Management Network",
description: "Property Management Network subscription",
type: "SERVICE",
category: "SOFTWARE",
})
if (!prodRes.ok) {
console.error("[paypal-setup] product creation failed:", await prodRes.text())
process.exit(1)
}
const product = await prodRes.json()
console.log(`[paypal-setup] Product: ${product.id}`)
const AMOUNTS = { pro: 29, landlord: 59 }
const envLines = []
for (const plan of ["pro", "landlord"]) {
for (const interval of ["month", "year"]) {
const amount = interval === "month" ? AMOUNTS[plan] : AMOUNTS[plan] * 10
const res = await post("/v1/billing/plans", {
product_id: product.id,
name: `${plan[0].toUpperCase()}${plan.slice(1)} ${interval === "month" ? "Monthly" : "Yearly"}`,
status: "ACTIVE",
billing_cycles: [
{
frequency: { interval_unit: interval === "month" ? "MONTH" : "YEAR", interval_count: 1 },
tenure_type: "REGULAR",
sequence: 1,
total_cycles: 0,
pricing_scheme: { fixed_price: { value: amount.toFixed(2), currency_code: "USD" } },
},
],
payment_preferences: {
auto_bill_outstanding: true,
setup_fee_failure_action: "CONTINUE",
payment_failure_threshold: 2,
},
})
if (!res.ok) {
console.error(`[paypal-setup] plan ${plan}/${interval} failed:`, await res.text())
continue
}
const p = await res.json()
const key = `PAYPAL_${plan.toUpperCase()}_${interval === "month" ? "MONTHLY" : "YEARLY"}_PLAN_ID`
console.log(` ${plan}/${interval} $${amount}${p.id}`)
envLines.push(`${key}=${p.id}`)
}
}
console.log("\n[paypal-setup] Add these to your environment:\n")
console.log(envLines.join("\n"))