/*****************************************************
🚀 SERVER.JS - GO HIGH LEVEL SAAS BACKEND
*****************************************************/
import dotenv from "dotenv";
dotenv.config();
import express from "express";
import cors from "cors";
import https from "https";
import pg from "pg";
import fetch from "node-fetch";
import { google } from "googleapis";
import { Client } from "@microsoft/microsoft-graph-client";
import "isomorphic-fetch";
const { Pool } = pg;
const app = express();
app.use(cors());
app.use(express.json());
/*****************************************************
1️⃣ DATABASE CONFIGURATION
*****************************************************/
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false },
});
/*****************************************************
2️⃣ GLOBAL OAUTH2 CLIENT FOR GOOGLE
*****************************************************/
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_OAUTH_CLIENT_ID,
process.env.GOOGLE_OAUTH_CLIENT_SECRET,
process.env.GOOGLE_OAUTH_REDIRECT_URL
);
/*****************************************************
3️⃣ HELPER FUNCTIONS
*****************************************************/
// Travel time cache
const travelTimeCache = new Map();
/* 🔐 Get Maps API Key (Fail Hard) */
async function getMapsKey(locationId) {
const res = await pool.query(
"SELECT maps_api_key FROM users WHERE id = $1",
[locationId]
);
if (!res.rows.length || !res.rows[0].maps_api_key) {
throw new Error("Maps API key not configured.");
}
return res.rows[0].maps_api_key;
}
/* 🔎 Validate Maps API Key */
async function validateMapsKey(key) {
try {
const testUrl = `https://maps.googleapis.com/maps/api/geocode/json?address=New+York&key=${key}`;
const resp = await fetch(testUrl);
const data = await resp.json();
return data.status === "OK" || data.status === "ZERO_RESULTS";
} catch {
return false;
}
}
/* 🚗 Travel Time Calculation */
async function getTravelTime(origin, destination, mapsApiKey) {
if (!origin || !destination || !mapsApiKey) return 15;
const cacheKey = `${origin}|${destination}`;
if (travelTimeCache.has(cacheKey)) {
return travelTimeCache.get(cacheKey);
}
const url = `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${encodeURIComponent(origin)}&destinations=${encodeURIComponent(destination)}&departure_time=now&key=${mapsApiKey}`;
return new Promise((resolve) => {
https.get(url, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
const json = JSON.parse(data);
if (json.rows?.[0]?.elements?.[0]?.status === "OK") {
const minutes =
Math.ceil(
json.rows[0].elements[0].duration.value / 60 / 5
) * 5;
travelTimeCache.set(cacheKey, minutes);
resolve(minutes);
} else resolve(15);
} catch {
resolve(15);
}
});
}).on("error", () => resolve(15));
});
}
/* 📈 Peak Multiplier Fallback */
function getPeakMultiplier(dateISO) {
const date = new Date(dateISO);
const hour = date.getHours();
const day = date.getDay();
if (day >= 1 && day <= 5) {
if ((hour >= 7 && hour <= 9) || (hour >= 16 && hour <= 19)) {
return 1.35;
}
}
if (day === 0 || day === 6) return 1.15;
return 1.0;
}
/*****************************************************
4️⃣ BUSINESS PROFILE (MAPS API SAVING)
*****************************************************/
app.post("/api/update-profile", async (req, res) => {
try {
const { userId, business_name, address, phone, maps_api_key } = req.body;
if (!userId) {
return res.status(400).json({ error: "User ID required." });
}
// Optional validation
if (maps_api_key) {
const isValid = await validateMapsKey(maps_api_key);
if (!isValid) {
return res.status(400).json({
error: "Invalid Google Maps API key.",
});
}
}
await pool.query(
`UPDATE users
SET business_name=$1,
address=$2,
phone=$3,
maps_api_key=$4
WHERE id=$5`,
[business_name, address, phone, maps_api_key, userId]
);
res.json({ success: true, message: "Business profile updated." });
} catch (err) {
console.error("Profile Update Error:", err);
res.status(500).json({ error: "Profile update failed." });
}
});
/*****************************************************
5️⃣ AVAILABILITY ENGINE
*****************************************************/
app.post("/api/availability", async (req, res) => {
try {
const { saas_location_staff_id, pickup, dropoff, date } = req.body;
if (!saas_location_staff_id || !date) {
return res.status(400).json({
slots: [],
error: "Missing required data.",
});
}
const [saas_location_id] =
saas_location_staff_id.split("_");
const userRes = await pool.query(
"SELECT * FROM users WHERE id=$1",
[saas_location_id]
);
if (!userRes.rows.length) {
return res.json({ slots: [], error: "User not found" });
}
const userConfig = userRes.rows[0];
// 🔐 Require Maps API Key
if (!userConfig.maps_api_key) {
return res.status(400).json({
slots: [],
error:
"Google Maps API key missing. Please add it in Business Profile.",
});
}
const svcRes = await pool.query(
"SELECT * FROM services WHERE saas_location_staff_id=$1 LIMIT 1",
[saas_location_staff_id]
);
if (!svcRes.rows.length) {
return res.json({ slots: [], error: "Service not found" });
}
const service = svcRes.rows[0];
const durationMin = service.duration_min || 60;
const bufferMin = service.outbound_buffer_min || 15;
const minNotice = service.min_notice_min || 120;
const slots = [];
const dayStart = new Date(`${date}T00:00:00`);
const dayEnd = new Date(`${date}T23:59:59`);
const earliestAllowed = new Date(
Date.now() + minNotice * 60000
);
for (
let t = new Date(dayStart);
new Date(t.getTime() + durationMin * 60000) <= dayEnd;
t.setMinutes(t.getMinutes() + 30)
) {
const start = new Date(t);
if (start < earliestAllowed) continue;
slots.push({
time: start.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
startISO: start.toISOString(),
});
}
res.json({ slots, waitlist: slots.length === 0 });
} catch (err) {
console.error("Availability Error:", err);
res.status(500).json({ error: err.message });
}
});
/*****************************************************
6️⃣ BOOKING ENGINE
*****************************************************/
app.post("/api/book", async (req, res) => {
const client = await pool.connect();
try {
const {
saas_location_staff_id,
startISO,
pickup,
dropoff,
email,
firstName,
lastName,
phone,
} = req.body;
const [saas_location_id] =
saas_location_staff_id.split("_");
const userRes = await client.query(
"SELECT * FROM users WHERE id=$1",
[saas_location_id]
);
const userConfig = userRes.rows[0];
// 🔐 Require Maps API Key
if (!userConfig.maps_api_key) {
return res.status(400).json({
error:
"Google Maps API key missing. Please add it in Business Profile.",
});
}
const svcQuery = await client.query(
"SELECT * FROM services WHERE saas_location_staff_id=$1 LIMIT 1",
[saas_location_staff_id]
);
const service = svcQuery.rows[0];
await client.query("BEGIN");
const mapsApiKey = await getMapsKey(saas_location_id);
const mapsUrl = `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${encodeURIComponent(
pickup
)}&destinations=${encodeURIComponent(
dropoff
)}&departure_time=now&key=${mapsApiKey}`;
const mapsResp = await fetch(mapsUrl);
const mapsData = await mapsResp.json();
let miles = 0;
if (mapsData.rows?.[0]?.elements?.[0]?.status === "OK") {
miles =
mapsData.rows[0].elements[0].distance.value /
1609.34;
}
let finalPrice =
parseFloat(service.base_rate || 50) +
miles * parseFloat(service.per_mile_rate || 3);
const multiplier =
userConfig.peak_multiplier ||
getPeakMultiplier(startISO);
const totalPrice = Math.ceil(
finalPrice * multiplier
);
const startTime = new Date(startISO);
const endTime = new Date(
startTime.getTime() +
(service.duration_min || 60) * 60000
);
await client.query(
`INSERT INTO bookings (
user_id, service_id, start_time, end_time,
pickup_address, dropoff_address,
customer_email, first_name, last_name,
phone, status, total_price
)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'confirmed',$11)`,
[
saas_location_id,
service.id,
startTime,
endTime,
pickup,
dropoff,
email,
firstName,
lastName,
phone,
totalPrice,
]
);
await client.query("COMMIT");
res.json({
success: true,
message: "Booking confirmed.",
totalPrice,
});
} catch (err) {
await client.query("ROLLBACK");
console.error("Booking Error:", err);
res.status(500).json({ error: err.message });
} finally {
client.release();
}
});
/*****************************************************
7️⃣ START SERVER
*****************************************************/
const PORT = process.env.PORT || 8080;
app.listen(PORT, () =>
console.log(`🚀 SaaS Backend running on ${PORT}`)
);