Bernt Christian Egeland 1 месяц назад
Родитель
Сommit
ae41c7c1aa

+ 60 - 46
prisma/seed_dummy_data.ts

@@ -629,8 +629,8 @@ async function seed() {
   console.log("\nApplying workshop settings...");
   const settings: Record<string, string> = {
     "workshop.address": "1450 Foundry Road, Denver, CO 80216",
-    "workshop.phone": "+1 (555) 123-4567",
-    "workshop.email": "service@egelandauto.com",
+    "workshop.phone": "+1 (555) 555-0100",
+    "workshop.email": "service@egelandauto.example.com",
     "workshop.serviceType": "automotive",
     "workshop.unitSystem": "imperial",
     "workshop.defaultLaborRate": "110",
@@ -667,6 +667,17 @@ async function seed() {
     "portal.enabled": "true",
     "portal.description": "Book a service, follow your repair and read your invoices online.",
     "portal.hours": "Mon-Fri 07:00-17:00 · Sat 09:00-13:00 · Closed Sunday",
+    // Placeholder provider config so the messages and Telegram pages render as
+    // a configured workshop rather than an empty setup prompt. The credentials
+    // are deliberately not real, and nothing can send from the demo anyway:
+    // assertOutboundAllowed() in lib/email, lib/sms and lib/telegram refuses
+    // every outbound call while DEMO_MODE=true.
+    "sms.provider": "twilio",
+    "sms.phoneNumber": "+15555550100",
+    "sms.twilio.accountSid": "ACdemo0000000000000000000000000000",
+    "sms.twilio.authToken": "demo-token-not-a-real-credential",
+    "telegram.botToken": "0000000000:DEMO-not-a-real-bot-token",
+    "telegram.botUsername": "EgelandAutoDemoBot",
   };
   await Promise.all(
     Object.entries(settings).map(([key, value]) =>
@@ -682,26 +693,29 @@ async function seed() {
   // -- Customers (20) --
   console.log("\nCreating customers...");
   const customers = await Promise.all([
-    /* 0  */ prisma.customer.create({ data: { customerNumber: "1001", name: "Summit Construction Inc.", email: "fleet@summitconstruction.com", phone: "+1 (555) 100-2000", address: "742 Industrial Pkwy, Denver, CO 80216", company: "Summit Construction Inc.", notes: "Major fleet customer. 15 vehicles + heavy machinery. Net-30 terms.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 1  */ prisma.customer.create({ data: { customerNumber: "1002", name: "James Mitchell", email: "james.mitchell@gmail.com", phone: "+1 (555) 201-3344", address: "1824 Maple Ave, Austin, TX 78701", notes: "Personal vehicles. Prefers synthetic oil.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 2  */ prisma.customer.create({ data: { customerNumber: "1003", name: "Pacific Freight Lines", email: "service@pacificfreight.com", phone: "+1 (555) 302-5566", address: "9100 Logistics Blvd, Portland, OR 97201", company: "Pacific Freight Lines LLC", notes: "Long-haul trucking company. 8 semi-trucks. Priority service.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 3  */ prisma.customer.create({ data: { customerNumber: "1004", name: "Sarah Coleman", email: "sarah.coleman@outlook.com", phone: "+1 (555) 403-7788", address: "567 Oak Street, Nashville, TN 37203", notes: "BMW and Porsche owner. Always requests OEM parts.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 4  */ prisma.customer.create({ data: { customerNumber: "1005", name: "Metro City Services", email: "fleet@metrocityservices.gov", phone: "+1 (555) 504-9900", address: "100 City Hall Plaza, Boston, MA 02108", company: "Metro City Services", notes: "Municipal fleet contract. Quarterly invoicing. Purchase order required.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 5  */ prisma.customer.create({ data: { customerNumber: "1006", name: "Ryan Parker", email: "ryan.parker@icloud.com", phone: "+1 (555) 605-1122", address: "2250 Cedar Lane, Seattle, WA 98101", notes: "Tesla and Subaru owner. Tech-savvy.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 6  */ prisma.customer.create({ data: { customerNumber: "1007", name: "Ironclad Equipment Co.", email: "parts@ironcladequip.com", phone: "+1 (555) 706-3344", address: "4500 Heavy Machinery Rd, Houston, TX 77001", company: "Ironclad Equipment Co.", notes: "Heavy equipment dealer. Sends us overflow work. Good referral source.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 7  */ prisma.customer.create({ data: { customerNumber: "1008", name: "David Chen", email: "david.chen@gmail.com", phone: "+1 (555) 807-5566", address: "890 Pine Street, San Francisco, CA 94102", notes: "Audi and VW owner. Referred by James Mitchell.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 8  */ prisma.customer.create({ data: { customerNumber: "1009", name: "Ridgeline Excavation LLC", email: "ops@ridgelineexcavation.com", phone: "+1 (555) 908-7788", address: "1200 Quarry Rd, Salt Lake City, UT 84101", company: "Ridgeline Excavation LLC", notes: "5 excavators, 2 wheel loaders, 3 dump trucks. Monthly maintenance contract.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 9  */ prisma.customer.create({ data: { customerNumber: "1010", name: "Amanda Foster", email: "amanda.foster@hotmail.com", phone: "+1 (555) 109-9900", address: "334 Birch Drive, Charlotte, NC 28202", notes: "Honda Civic owner. Budget-conscious, prefers aftermarket.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 10 */ prisma.customer.create({ data: { customerNumber: "1011", name: "Prairie Farms Co-op", email: "maintenance@prairiefarms.com", phone: "+1 (555) 210-1122", address: "Route 66 Farm Road, Omaha, NE 68101", company: "Prairie Farms Co-op", notes: "Agricultural equipment. Tractors, harvesters. Seasonal peaks spring/fall.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 11 */ prisma.customer.create({ data: { customerNumber: "1012", name: "Mike Thompson", email: "mike.thompson@yahoo.com", phone: "+1 (555) 311-3344", address: "445 Elm Court, Minneapolis, MN 55401", notes: "Ford F-150 and Jeep owner. Off-road enthusiast.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 12 */ prisma.customer.create({ data: { customerNumber: "1013", name: "Apex Contractors Group", email: "service@apexcontractors.com", phone: "+1 (555) 412-5566", address: "2800 Builder's Row, Atlanta, GA 30301", company: "Apex Contractors Group", notes: "Small contractor. 2 excavators, 1 dump truck, 2 vans.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 13 */ prisma.customer.create({ data: { customerNumber: "1014", name: "Jessica Rivera", email: "jessica.r@protonmail.com", phone: "+1 (555) 513-7788", address: "1100 Sunset Blvd, Miami, FL 33101", notes: "Tesla Model Y owner. Referred by Ryan Parker.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 14 */ prisma.customer.create({ data: { customerNumber: "1015", name: "Titan Building Corp.", email: "projects@titanbuildingcorp.com", phone: "+1 (555) 614-9900", address: "5000 Commerce Dr, Dallas, TX 75201", company: "Titan Building Corp.", notes: "Large construction firm. 20+ pieces of equipment. Net-45 terms.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 15 */ prisma.customer.create({ data: { customerNumber: "1016", name: "Kevin O'Brien", email: "kevin.obrien@gmail.com", phone: "+1 (555) 715-1122", address: "678 Market Street, Philadelphia, PA 19101", notes: "Mercedes Sprinter van for his catering business.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 16 */ prisma.customer.create({ data: { customerNumber: "1017", name: "Greenfield Waste Management", email: "dispatch@greenfieldwaste.com", phone: "+1 (555) 816-3344", address: "7700 Recycling Way, Phoenix, AZ 85001", company: "Greenfield Waste Management", notes: "Waste management fleet. 12 trucks. Quarterly PM schedule.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 17 */ prisma.customer.create({ data: { customerNumber: "1018", name: "Robert Hughes", email: "robert.hughes@outlook.com", phone: "+1 (555) 917-5566", address: "2100 Lakeview Ave, Detroit, MI 48201", notes: "Ram truck and VW Golf owner. Weekend car guy.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 17 */ prisma.customer.create({ data: { customerNumber: "1019", name: "Heartland Grain Co.", email: "ops@heartlandgrain.com", phone: "+1 (555) 018-7788", address: "4400 Harvest Rd, Des Moines, IA 50301", company: "Heartland Grain Co.", notes: "Large-scale farming. 6 tractors, 2 combines. Seasonal rush spring and fall.", userId: USER_ID, organizationId: ORG_ID } }),
-    /* 19 */ prisma.customer.create({ data: { customerNumber: "1020", name: "Lisa Martinez", email: "lisa.martinez@icloud.com", phone: "+1 (555) 119-9900", address: "425 Ranch Road, San Antonio, TX 78201", notes: "Chevy Silverado and Toyota Tacoma. Ranch use.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 0  */ prisma.customer.create({ data: { customerNumber: "1001", name: "Summit Construction Inc.", email: "fleet@summitconstruction.example.com", phone: "+1 (555) 555-0101", address: "742 Industrial Pkwy, Denver, CO 80216", company: "Summit Construction Inc.", notes: "Major fleet customer. 15 vehicles + heavy machinery. Net-30 terms.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 1  */ prisma.customer.create({ data: { customerNumber: "1002", name: "James Mitchell", email: "james.mitchell@example.com", phone: "+1 (555) 555-0102", address: "1824 Maple Ave, Austin, TX 78701", notes: "Personal vehicles. Prefers synthetic oil.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 2  */ prisma.customer.create({ data: { customerNumber: "1003", name: "Pacific Freight Lines", email: "service@pacificfreight.example.com", phone: "+1 (555) 555-0103", address: "9100 Logistics Blvd, Portland, OR 97201", company: "Pacific Freight Lines LLC", notes: "Long-haul trucking company. 8 semi-trucks. Priority service.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 3  */ prisma.customer.create({ data: { customerNumber: "1004", name: "Sarah Coleman", email: "sarah.coleman@example.com", phone: "+1 (555) 555-0104", address: "567 Oak Street, Nashville, TN 37203", notes: "BMW and Porsche owner. Always requests OEM parts.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 4  */ prisma.customer.create({ data: { customerNumber: "1005", name: "Metro City Services", email: "fleet@metrocityservices.example.com", phone: "+1 (555) 555-0105", address: "100 City Hall Plaza, Boston, MA 02108", company: "Metro City Services", notes: "Municipal fleet contract. Quarterly invoicing. Purchase order required.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 5  */ prisma.customer.create({ data: { customerNumber: "1006", name: "Ryan Parker", email: "ryan.parker@example.com", phone: "+1 (555) 555-0106", address: "2250 Cedar Lane, Seattle, WA 98101", notes: "Tesla and Subaru owner. Tech-savvy.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 6  */ prisma.customer.create({ data: { customerNumber: "1007", name: "Ironclad Equipment Co.", email: "parts@ironcladequip.example.com", phone: "+1 (555) 555-0107", address: "4500 Heavy Machinery Rd, Houston, TX 77001", company: "Ironclad Equipment Co.", notes: "Heavy equipment dealer. Sends us overflow work. Good referral source.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 7  */ prisma.customer.create({ data: { customerNumber: "1008", name: "David Chen", email: "david.chen@example.com", phone: "+1 (555) 555-0108", address: "890 Pine Street, San Francisco, CA 94102", notes: "Audi and VW owner. Referred by James Mitchell.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 8  */ prisma.customer.create({ data: { customerNumber: "1009", name: "Ridgeline Excavation LLC", email: "ops@ridgelineexcavation.example.com", phone: "+1 (555) 555-0109", address: "1200 Quarry Rd, Salt Lake City, UT 84101", company: "Ridgeline Excavation LLC", notes: "5 excavators, 2 wheel loaders, 3 dump trucks. Monthly maintenance contract.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 9  */ prisma.customer.create({ data: { customerNumber: "1010", name: "Amanda Foster", email: "amanda.foster@example.com", phone: "+1 (555) 555-0110", address: "334 Birch Drive, Charlotte, NC 28202", notes: "Honda Civic owner. Budget-conscious, prefers aftermarket.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 10 */ prisma.customer.create({ data: { customerNumber: "1011", name: "Prairie Farms Co-op", email: "maintenance@prairiefarms.example.com", phone: "+1 (555) 555-0111", address: "Route 66 Farm Road, Omaha, NE 68101", company: "Prairie Farms Co-op", notes: "Agricultural equipment. Tractors, harvesters. Seasonal peaks spring/fall.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 11 */ prisma.customer.create({ data: { customerNumber: "1012", name: "Mike Thompson", email: "mike.thompson@example.com", phone: "+1 (555) 555-0112", address: "445 Elm Court, Minneapolis, MN 55401", notes: "Ford F-150 and Jeep owner. Off-road enthusiast.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 12 */ prisma.customer.create({ data: { customerNumber: "1013", name: "Apex Contractors Group", email: "service@apexcontractors.example.com", phone: "+1 (555) 555-0113", address: "2800 Builder's Row, Atlanta, GA 30301", company: "Apex Contractors Group", notes: "Small contractor. 2 excavators, 1 dump truck, 2 vans.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 13 */ prisma.customer.create({ data: { customerNumber: "1014", name: "Jessica Rivera", email: "jessica.r@example.com", phone: "+1 (555) 555-0114", address: "1100 Sunset Blvd, Miami, FL 33101", notes: "Tesla Model Y owner. Referred by Ryan Parker.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 14 */ prisma.customer.create({ data: { customerNumber: "1015", name: "Titan Building Corp.", email: "projects@titanbuildingcorp.example.com", phone: "+1 (555) 555-0115", address: "5000 Commerce Dr, Dallas, TX 75201", company: "Titan Building Corp.", notes: "Large construction firm. 20+ pieces of equipment. Net-45 terms.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 15 */ prisma.customer.create({ data: { customerNumber: "1016", name: "Kevin O'Brien", email: "kevin.obrien@example.com", phone: "+1 (555) 555-0116", address: "678 Market Street, Philadelphia, PA 19101", notes: "Mercedes Sprinter van for his catering business.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 16 */ prisma.customer.create({ data: { customerNumber: "1017", name: "Greenfield Waste Management", email: "dispatch@greenfieldwaste.example.com", phone: "+1 (555) 555-0117", address: "7700 Recycling Way, Phoenix, AZ 85001", company: "Greenfield Waste Management", notes: "Waste management fleet. 12 trucks. Quarterly PM schedule.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 17 */ prisma.customer.create({ data: { customerNumber: "1018", name: "Robert Hughes", email: "robert.hughes@example.com", phone: "+1 (555) 555-0118", address: "2100 Lakeview Ave, Detroit, MI 48201", notes: "Ram truck and VW Golf owner. Weekend car guy.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 17 */ prisma.customer.create({ data: { customerNumber: "1019", name: "Heartland Grain Co.", email: "ops@heartlandgrain.example.com", phone: "+1 (555) 555-0119", address: "4400 Harvest Rd, Des Moines, IA 50301", company: "Heartland Grain Co.", notes: "Large-scale farming. 6 tractors, 2 combines. Seasonal rush spring and fall.", userId: USER_ID, organizationId: ORG_ID } }),
+    /* 19 */ prisma.customer.create({ data: { customerNumber: "1020", name: "Lisa Martinez", email: "lisa.martinez@example.com", phone: "+1 (555) 555-0120", address: "425 Ranch Road, San Antonio, TX 78201", notes: "Chevy Silverado and Toyota Tacoma. Ranch use.", userId: USER_ID, organizationId: ORG_ID } }),
+    // The one address on a domain we own, for checking a send end to end
+    // without it ever leaving Torqvoice. Everyone else sits on example.com.
+    /* 20 */ prisma.customer.create({ data: { customerNumber: "1021", name: "Jennifer Hayes", email: "jennifer@torqvoice.com", phone: "+1 (555) 555-0121", address: "88 Copper Lane, Boulder, CO 80301", notes: "Test account for message delivery checks.", userId: USER_ID, organizationId: ORG_ID } }),
   ]);
   console.log(`  Created ${customers.length} customers`);
 
@@ -1255,30 +1269,30 @@ async function seed() {
 
   // -- SMS Messages --
   console.log("\nCreating SMS messages...");
-  const shopNumber = "+15551234567";
+  const shopNumber = "+15555550100";
   await Promise.all([
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15551002000", body: "Hi Summit Construction, your Ford F-150 (CO-4455) transmission flush is complete. Total: $689.04. Ready for pickup.", status: "delivered", organizationId: ORG_ID, customerId: customers[0].id } }),
-    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15551002000", toNumber: shopNumber, body: "Thanks! We'll send someone before 4pm. Can you look at the D6 bulldozer next week? Blade cylinder is leaking again.", status: "received", organizationId: ORG_ID, customerId: customers[0].id, createdAt: new Date(Date.now() - 3500000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15551002000", body: "Absolutely, we can fit the D6 in on Tuesday. We'll order the seal kit ahead of time.", status: "delivered", organizationId: ORG_ID, customerId: customers[0].id, createdAt: new Date(Date.now() - 3400000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15552013344", body: "Hi James, your Toyota Camry is due for its 30K service. We have Thursday or Friday availability.", status: "delivered", organizationId: ORG_ID, customerId: customers[1].id, createdAt: new Date(Date.now() - 86400000) } }),
-    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15552013344", toNumber: shopNumber, body: "Thursday morning works. Synthetic oil like last time please.", status: "received", organizationId: ORG_ID, customerId: customers[1].id, createdAt: new Date(Date.now() - 82800000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15552013344", body: "Thursday 8am confirmed. Full synthetic 0W-20 as always. See you then!", status: "delivered", organizationId: ORG_ID, customerId: customers[1].id, createdAt: new Date(Date.now() - 82000000) } }),
-    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15553025566", toNumber: shopNumber, body: "Our Volvo FH 640 (unit #5) is showing a check engine light. Can you get it in ASAP?", status: "received", organizationId: ORG_ID, customerId: customers[2].id, createdAt: new Date(Date.now() - 172800000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15553025566", body: "We can take the Volvo FH 640 first thing tomorrow. Have the driver bring it in by 7am.", status: "delivered", organizationId: ORG_ID, customerId: customers[2].id, createdAt: new Date(Date.now() - 172000000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15553025566", body: "Update on the Volvo FH 640: DOT inspection in progress. Minor issues with marker lights and wipers. Done by 3pm.", status: "delivered", organizationId: ORG_ID, customerId: customers[2].id, createdAt: new Date(Date.now() - 86400000) } }),
-    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15552101122", toNumber: shopNumber, body: "Our John Deere 6R is running rough and hydraulics feel sluggish. Planting season starts in 3 weeks!", status: "received", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 432000000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15552101122", body: "Bring the John Deere 6R in this week, we'll bump it to priority. Likely needs 500hr service + hydraulic analysis.", status: "delivered", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 428400000) } }),
-    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15552101122", toNumber: shopNumber, body: "Great. The Massey Ferguson 8S has low hydraulic pressure too. Can you look at both?", status: "received", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 424800000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15552101122", body: "Bring both Wednesday. JD done same day, Massey Thursday. Both ready before planting.", status: "delivered", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 421200000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15557151122", body: "Kevin, your Sprinter turbo has excessive shaft play causing black smoke. Replacement: $1,280 + labor. Proceed?", status: "delivered", organizationId: ORG_ID, customerId: customers[15].id, createdAt: new Date(Date.now() - 345600000) } }),
-    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15557151122", toNumber: shopNumber, body: "Ouch. Yeah go ahead, I need the van for catering events next week.", status: "received", organizationId: ORG_ID, customerId: customers[15].id, createdAt: new Date(Date.now() - 342000000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15557151122", body: "Parts arrive Thursday, done Friday afternoon. Total: $2,243.70 including oil change.", status: "delivered", organizationId: ORG_ID, customerId: customers[15].id, createdAt: new Date(Date.now() - 338400000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15559087788", body: "Ridgeline, the Komatsu PC210 track chains are done. Both sides + new sprockets. Total: $9,309.60. Ready for transport.", status: "delivered", organizationId: ORG_ID, customerId: customers[8].id, createdAt: new Date(Date.now() - 604800000) } }),
-    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15559087788", toNumber: shopNumber, body: "Excellent work. Sending the lowboy tomorrow. Also sending the Volvo EC220E for 1000hr service.", status: "received", organizationId: ORG_ID, customerId: customers[8].id, createdAt: new Date(Date.now() - 601200000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15559087788", body: "We have Volvo EC220E filters in stock. 1-2 day turnaround once it arrives.", status: "delivered", organizationId: ORG_ID, customerId: customers[8].id, createdAt: new Date(Date.now() - 597600000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15554037788", body: "Hi Sarah, your Porsche 911 annual service is complete. Total: $832.14. All OEM parts as always.", status: "delivered", organizationId: ORG_ID, customerId: customers[3].id, createdAt: new Date(Date.now() - 259200000) } }),
-    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15554037788", toNumber: shopNumber, body: "Perfect, thank you. I also want to get a quote on the BMW brake job with OEM rotors.", status: "received", organizationId: ORG_ID, customerId: customers[3].id, createdAt: new Date(Date.now() - 255600000) } }),
-    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15554037788", body: "Quote sent to your email - $1,711.80 for the full brake overhaul with OEM rotors and pads. Valid until March 15.", status: "delivered", organizationId: ORG_ID, customerId: customers[3].id, createdAt: new Date(Date.now() - 254000000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550101", body: "Hi Summit Construction, your Ford F-150 (CO-4455) transmission flush is complete. Total: $689.04. Ready for pickup.", status: "delivered", organizationId: ORG_ID, customerId: customers[0].id } }),
+    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550101", toNumber: shopNumber, body: "Thanks! We'll send someone before 4pm. Can you look at the D6 bulldozer next week? Blade cylinder is leaking again.", status: "received", organizationId: ORG_ID, customerId: customers[0].id, createdAt: new Date(Date.now() - 3500000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550101", body: "Absolutely, we can fit the D6 in on Tuesday. We'll order the seal kit ahead of time.", status: "delivered", organizationId: ORG_ID, customerId: customers[0].id, createdAt: new Date(Date.now() - 3400000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550102", body: "Hi James, your Toyota Camry is due for its 30K service. We have Thursday or Friday availability.", status: "delivered", organizationId: ORG_ID, customerId: customers[1].id, createdAt: new Date(Date.now() - 86400000) } }),
+    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550102", toNumber: shopNumber, body: "Thursday morning works. Synthetic oil like last time please.", status: "received", organizationId: ORG_ID, customerId: customers[1].id, createdAt: new Date(Date.now() - 82800000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550102", body: "Thursday 8am confirmed. Full synthetic 0W-20 as always. See you then!", status: "delivered", organizationId: ORG_ID, customerId: customers[1].id, createdAt: new Date(Date.now() - 82000000) } }),
+    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550103", toNumber: shopNumber, body: "Our Volvo FH 640 (unit #5) is showing a check engine light. Can you get it in ASAP?", status: "received", organizationId: ORG_ID, customerId: customers[2].id, createdAt: new Date(Date.now() - 172800000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550103", body: "We can take the Volvo FH 640 first thing tomorrow. Have the driver bring it in by 7am.", status: "delivered", organizationId: ORG_ID, customerId: customers[2].id, createdAt: new Date(Date.now() - 172000000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550103", body: "Update on the Volvo FH 640: DOT inspection in progress. Minor issues with marker lights and wipers. Done by 3pm.", status: "delivered", organizationId: ORG_ID, customerId: customers[2].id, createdAt: new Date(Date.now() - 86400000) } }),
+    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550111", toNumber: shopNumber, body: "Our John Deere 6R is running rough and hydraulics feel sluggish. Planting season starts in 3 weeks!", status: "received", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 432000000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550111", body: "Bring the John Deere 6R in this week, we'll bump it to priority. Likely needs 500hr service + hydraulic analysis.", status: "delivered", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 428400000) } }),
+    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550111", toNumber: shopNumber, body: "Great. The Massey Ferguson 8S has low hydraulic pressure too. Can you look at both?", status: "received", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 424800000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550111", body: "Bring both Wednesday. JD done same day, Massey Thursday. Both ready before planting.", status: "delivered", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 421200000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550116", body: "Kevin, your Sprinter turbo has excessive shaft play causing black smoke. Replacement: $1,280 + labor. Proceed?", status: "delivered", organizationId: ORG_ID, customerId: customers[15].id, createdAt: new Date(Date.now() - 345600000) } }),
+    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550116", toNumber: shopNumber, body: "Ouch. Yeah go ahead, I need the van for catering events next week.", status: "received", organizationId: ORG_ID, customerId: customers[15].id, createdAt: new Date(Date.now() - 342000000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550116", body: "Parts arrive Thursday, done Friday afternoon. Total: $2,243.70 including oil change.", status: "delivered", organizationId: ORG_ID, customerId: customers[15].id, createdAt: new Date(Date.now() - 338400000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550109", body: "Ridgeline, the Komatsu PC210 track chains are done. Both sides + new sprockets. Total: $9,309.60. Ready for transport.", status: "delivered", organizationId: ORG_ID, customerId: customers[8].id, createdAt: new Date(Date.now() - 604800000) } }),
+    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550109", toNumber: shopNumber, body: "Excellent work. Sending the lowboy tomorrow. Also sending the Volvo EC220E for 1000hr service.", status: "received", organizationId: ORG_ID, customerId: customers[8].id, createdAt: new Date(Date.now() - 601200000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550109", body: "We have Volvo EC220E filters in stock. 1-2 day turnaround once it arrives.", status: "delivered", organizationId: ORG_ID, customerId: customers[8].id, createdAt: new Date(Date.now() - 597600000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550104", body: "Hi Sarah, your Porsche 911 annual service is complete. Total: $832.14. All OEM parts as always.", status: "delivered", organizationId: ORG_ID, customerId: customers[3].id, createdAt: new Date(Date.now() - 259200000) } }),
+    prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550104", toNumber: shopNumber, body: "Perfect, thank you. I also want to get a quote on the BMW brake job with OEM rotors.", status: "received", organizationId: ORG_ID, customerId: customers[3].id, createdAt: new Date(Date.now() - 255600000) } }),
+    prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550104", body: "Quote sent to your email - $1,711.80 for the full brake overhaul with OEM rotors and pads. Valid until March 15.", status: "delivered", organizationId: ORG_ID, customerId: customers[3].id, createdAt: new Date(Date.now() - 254000000) } }),
   ]);
   console.log("  Created 22 SMS messages");
 
@@ -2014,7 +2028,7 @@ async function seed() {
       inspectionId: inspections[7].id,
       organizationId: ORG_ID,
       status: "quoted",
-      message: "Send the weld repair cost to ops@ridgelineexcavation.com.",
+      message: "Send the weld repair cost to ops@ridgelineexcavation.example.com.",
       selectedItemIds: [],
       createdAt: hoursAgo(52),
     },

+ 33 - 0
src/__tests__/lib/demo-outbound.test.ts

@@ -0,0 +1,33 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+
+describe("demo mode blocks every outbound transport", () => {
+  beforeEach(() => { vi.resetModules(); process.env.DEMO_MODE = "true"; });
+  afterEach(() => { delete process.env.DEMO_MODE; });
+
+  it("refuses email, sms and telegram", async () => {
+    const { assertOutboundAllowed } = await import("@/lib/demo");
+    for (const ch of ["email", "sms", "telegram"] as const) {
+      expect(() => assertOutboundAllowed(ch)).toThrow(/disabled on the demo/);
+    }
+  });
+
+  it("allows them when demo mode is off", async () => {
+    process.env.DEMO_MODE = "false";
+    vi.resetModules();
+    const { assertOutboundAllowed } = await import("@/lib/demo");
+    expect(() => assertOutboundAllowed("email")).not.toThrow();
+  });
+
+  it("unlocks the feature set for the demo org", async () => {
+    const { getFeatures } = await import("@/lib/features");
+    const features = await getFeatures("any-org");
+    expect(features.sms).toBe(true);
+    expect(features.telegram).toBe(true);
+    expect(features.brandingRemoved).toBe(false);
+  });
+
+  it("stops the scheduled-message cron before it reads the queue", async () => {
+    const { processDueMessages } = await import("@/lib/cron/scheduled-messages");
+    expect(await processDueMessages()).toBe(0);
+  });
+});

+ 3 - 0
src/lib/cron/check-licenses.ts

@@ -1,4 +1,5 @@
 import { CronJob } from 'cron'
+import { isDemoMode } from '@/lib/demo'
 import { db } from '@/lib/db'
 import { sendOrgMail, getOrgFromAddress } from '@/lib/email'
 import { notify } from '@/lib/notify'
@@ -75,6 +76,8 @@ export async function revalidateOrganizationLicense(organizationId: string, lice
 }
 
 export async function sendExpiryWarning(organizationId: string, daysLeft: number) {
+  if (isDemoMode) return
+
   // Check if we already warned today
   const lastWarning = await db.appSetting.findUnique({
     where: { organizationId_key: { organizationId, key: 'license.lastExpiryWarning' } },

+ 4 - 0
src/lib/cron/low-stock-alerts.ts

@@ -1,4 +1,5 @@
 import { CronJob } from "cron";
+import { isDemoMode } from "@/lib/demo";
 import { db } from "@/lib/db";
 import { notify } from "@/lib/notify";
 import { sendOrgMail, getOrgFromAddress } from "@/lib/email";
@@ -154,6 +155,9 @@ async function sendDigest(
  * or an admin action without waiting for the schedule.
  */
 export async function processOrgLowStock(organizationId: string, now = new Date()) {
+  // The digest goes out by email, which the demo cannot send.
+  if (isDemoMode) return { skipped: true as const };
+
   const settingRows = await db.appSetting.findMany({
     where: {
       organizationId,

+ 5 - 0
src/lib/cron/reminder-alerts.ts

@@ -1,5 +1,6 @@
 import { CronJob } from "cron";
 import { db } from "@/lib/db";
+import { isDemoMode } from "@/lib/demo";
 import { notify } from "@/lib/notify";
 import { sendOrgMail, getOrgFromAddress } from "@/lib/email";
 
@@ -51,6 +52,10 @@ async function getRecipients(organizationId: string) {
 }
 
 async function sendReminderEmail(organizationId: string, reminders: DueReminder[]) {
+  // The bell notification above is the whole of the demo's reminder alerting.
+  // sendOrgMail would refuse anyway; returning here keeps the hourly run quiet.
+  if (isDemoMode) return;
+
   const recipients = await getRecipients(organizationId);
   if (recipients.length === 0) return;
 

+ 4 - 0
src/lib/cron/report-schedules.ts

@@ -1,4 +1,5 @@
 import { CronJob } from "cron";
+import { isDemoMode } from "@/lib/demo";
 import { db } from "@/lib/db";
 import { renderToBuffer } from "@react-pdf/renderer";
 import "@/features/vehicles/Components/invoice-pdf/fonts";
@@ -354,6 +355,9 @@ export async function processOneSchedule(schedule: {
   organizationId: string;
   endDate: Date | null;
 }) {
+  // Scheduled reports are emailed as attachments. Nothing leaves the demo.
+  if (isDemoMode) return;
+
   const now = new Date();
   const sections: string[] = JSON.parse(schedule.sections);
   const recipientIds: string[] = JSON.parse(schedule.recipients);

+ 6 - 0
src/lib/cron/scheduled-messages.ts

@@ -1,5 +1,6 @@
 import { CronJob } from "cron";
 import { db } from "@/lib/db";
+import { isDemoMode } from "@/lib/demo";
 import {
   dispatchScheduledMessage,
   nextSendAt,
@@ -18,6 +19,11 @@ const MAX_LATENESS_MS = 24 * 60 * 60 * 1000;
  * send worked or not, so a single failure doesn't stop the series.
  */
 export async function processDueMessages(now = new Date()): Promise<number> {
+  // The demo's seeded queue is scenery, not a work list. The transports refuse
+  // to send in demo mode anyway; stopping here keeps the queue looking alive
+  // instead of turning every seeded message red a minute after each reset.
+  if (isDemoMode) return 0;
+
   const due = await db.scheduledMessage.findMany({
     where: { status: "scheduled", sendAt: { lte: now } },
     select: {

+ 18 - 0
src/lib/demo.ts

@@ -27,6 +27,24 @@ export function demoGuard(): void {
   }
 }
 
+/**
+ * Hard stop on anything that would leave the box: email, SMS, Telegram.
+ *
+ * `demoGuard()` only covers server actions, so the background crons —
+ * scheduled messages, reminder alerts, report schedules, low-stock digests —
+ * went straight past it and out through the platform sender. This sits in the
+ * transports themselves, which is the one place every one of those paths has
+ * to go through. Seed data carries customer-looking addresses, so a demo reset
+ * is enough to queue mail at real inboxes without it.
+ */
+export function assertOutboundAllowed(channel: "email" | "sms" | "telegram"): void {
+  if (isDemoMode) {
+    throw new Error(
+      `Outbound ${channel} is disabled on the demo. Install Torqvoice on your own server to send for real.`,
+    );
+  }
+}
+
 /**
  * Setting keys that store provider credentials / secrets. Demo visitors
  * shouldn't be able to paste real API keys into a shared demo DB.

+ 3 - 0
src/lib/email.ts

@@ -6,6 +6,7 @@ import FormData from "form-data";
 import sgMail, { type MailDataRequired } from "@sendgrid/mail";
 import { SESClient, SendRawEmailCommand } from "@aws-sdk/client-ses";
 import { db } from "./db";
+import { assertOutboundAllowed } from "./demo";
 import { SYSTEM_SETTING_KEYS } from "@/features/admin/Schema/systemSettingsSchema";
 import { ORG_EMAIL_KEYS } from "@/features/email/Schema/emailSettingsSchema";
 
@@ -527,6 +528,7 @@ function sendWithProvider(
 // ─── System-level router (unchanged behavior) ──────────────────────────────
 
 export async function sendMail(options: SendMailOptions) {
+  assertOutboundAllowed("email");
   const provider = await getEmailProvider();
   await sendWithProvider(provider, options, new Map(), "system");
 }
@@ -610,6 +612,7 @@ export async function sendOrgMail(
   organizationId: string,
   options: SendMailOptions,
 ) {
+  assertOutboundAllowed("email");
   const provider = await getOrgEmailProvider(organizationId);
 
   if (!provider) {

+ 10 - 0
src/lib/features.ts

@@ -1,5 +1,6 @@
 import { cache } from 'react'
 import { db } from './db'
+import { isDemoMode } from './demo'
 
 export type Plan = 'free' | 'pro' | 'enterprise' | 'white-label'
 
@@ -122,6 +123,15 @@ export function isCloudMode(): boolean {
 const SUBSCRIPTION_GRACE_MS = 3 * 24 * 60 * 60 * 1000
 
 export const getFeatures = cache(async (organizationId: string): Promise<PlanFeatures> => {
+  // The demo exists to show the product, so nothing is plan-gated there — the
+  // messages, Telegram, portal and custom-field pages all need their feature
+  // flag on to render at all. Sending stays impossible regardless: the demo
+  // guard blocks the send actions and the transports refuse outright
+  // (assertOutboundAllowed in lib/email, lib/sms, lib/telegram).
+  if (isDemoMode) {
+    return { ...PLAN_FEATURES['white-label'], brandingRemoved: false, customPlatformName: false }
+  }
+
   if (isCloudMode()) {
     const subscription = await db.subscription.findUnique({
       where: { organizationId },

+ 2 - 0
src/lib/sms.ts

@@ -1,4 +1,5 @@
 import { db } from './db'
+import { assertOutboundAllowed } from './demo'
 import { ORG_SMS_KEYS } from '@/features/sms/Schema/smsSettingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { normalizePortalPhone } from './portal-phone'
@@ -228,6 +229,7 @@ export async function sendOrgSms(
   organizationId: string,
   options: SendSmsOptions
 ): Promise<SendSmsResult> {
+  assertOutboundAllowed('sms')
   const provider = await getOrgSmsProvider(organizationId)
   if (!provider) {
     throw new Error('SMS is not configured. Set up an SMS provider in Settings.')

+ 2 - 0
src/lib/telegram.ts

@@ -1,4 +1,5 @@
 import { db } from "./db";
+import { assertOutboundAllowed } from "./demo";
 import { ORG_TELEGRAM_KEYS } from "@/features/telegram/Schema/telegramSettingsSchema";
 
 const TELEGRAM_API_BASE = "https://api.telegram.org/bot";
@@ -78,6 +79,7 @@ export async function sendTelegramMessage(
   organizationId: string,
   options: SendTelegramMessageOptions,
 ): Promise<SendTelegramMessageResult> {
+  assertOutboundAllowed("telegram");
   const botToken = await getOrgTelegramBotToken(organizationId);
   if (!botToken) {
     throw new Error("Telegram bot is not configured. Set up a bot token in Settings.");