Salesforce Integration¶
Salesforce dient als führendes CRM für Kundenbeziehungen.
Sync-Strategie¶
┌─────────────────┐ ┌─────────────────┐
│ Salesforce │◄────────►│ MORELO Suite │
│ (Master CRM) │ 2-Way │ (D1 Cache) │
└─────────────────┘ Sync └─────────────────┘
| Richtung | Daten | Trigger |
|---|---|---|
| SF → Suite | Kontakte, Opportunities | Webhook / Polling |
| Suite → SF | Aktivitäten, Notizen | API Push |
Salesforce Objects¶
Contact (Kunden)¶
interface SalesforceContact {
Id: string;
FirstName: string;
LastName: string;
Email: string;
Phone: string;
MailingAddress: {
street: string;
city: string;
postalCode: string;
country: string;
};
// Custom Fields
MORELO_Customer_ID__c: string;
Preferred_Model__c: string;
Last_Visit__c: string;
}
Opportunity (Verkaufschance)¶
interface SalesforceOpportunity {
Id: string;
Name: string;
AccountId: string;
ContactId: string;
StageName: string; // Prospecting, Qualification, Proposal, Closed Won
Amount: number;
CloseDate: string;
// Custom Fields
Vehicle_Model__c: string;
Configuration_URL__c: string;
}
API Endpoints¶
REST API¶
# Kontakte abrufen
GET /services/data/v59.0/query
?q=SELECT+Id,FirstName,LastName,Email+FROM+Contact+WHERE+LastModifiedDate+>+2026-01-01T00:00:00Z
# Kontakt erstellen
POST /services/data/v59.0/sobjects/Contact
{
"FirstName": "Hans",
"LastName": "Mustermann",
"Email": "hans@email.de",
"MORELO_Customer_ID__c": "cust_abc123"
}
# Opportunity aktualisieren
PATCH /services/data/v59.0/sobjects/Opportunity/{id}
{
"StageName": "Proposal",
"Configuration_URL__c": "https://konfigurator.morelo.de/c/xyz"
}
Bulk API (für große Datenmengen)¶
# Bulk Job erstellen
POST /services/data/v59.0/jobs/ingest
{
"operation": "upsert",
"object": "Contact",
"externalIdFieldName": "MORELO_Customer_ID__c"
}
# CSV Daten hochladen
PUT /services/data/v59.0/jobs/ingest/{jobId}/batches
Content-Type: text/csv
MORELO_Customer_ID__c,FirstName,LastName,Email
cust_001,Hans,Mustermann,hans@email.de
cust_002,Maria,Schmidt,maria@email.de
SonicJS Integration¶
Route: /api/integrations/salesforce¶
// src/routes/integrations/salesforce.ts
import { Hono } from 'hono';
const app = new Hono();
// Salesforce OAuth Token
async function getAccessToken(env: Env) {
const cached = await env.KV.get('sf:token');
if (cached) return cached;
const response = await fetch(
`${env.SF_INSTANCE_URL}/services/oauth2/token`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: env.SF_CLIENT_ID,
client_secret: env.SF_CLIENT_SECRET
})
}
);
const { access_token, expires_in } = await response.json();
await env.KV.put('sf:token', access_token, {
expirationTtl: expires_in - 300
});
return access_token;
}
// Kontakte synchronisieren
app.post('/sync/contacts', async (c) => {
const token = await getAccessToken(c.env);
// Geänderte Kontakte seit letztem Sync
const lastSync = await c.env.KV.get('sf:lastSync') || '2020-01-01T00:00:00Z';
const query = `SELECT Id,FirstName,LastName,Email,Phone FROM Contact WHERE LastModifiedDate > ${lastSync}`;
const response = await fetch(
`${c.env.SF_INSTANCE_URL}/services/data/v59.0/query?q=${encodeURIComponent(query)}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const { records } = await response.json();
// In D1 speichern
for (const contact of records) {
await c.env.DB.prepare(`
INSERT OR REPLACE INTO customers (id, email, first_name, last_name, salesforce_id)
VALUES (?, ?, ?, ?, ?)
`).bind(
crypto.randomUUID(),
contact.Email,
contact.FirstName,
contact.LastName,
contact.Id
).run();
}
await c.env.KV.put('sf:lastSync', new Date().toISOString());
return c.json({ synced: records.length });
});
export default app;
Webhooks (Outbound Messages)¶
Salesforce kann bei Änderungen Webhooks senden:
// Webhook Endpoint
app.post('/webhooks/salesforce', async (c) => {
const xml = await c.req.text();
// SOAP XML parsen
const contactId = extractFromXml(xml, 'sf:Id');
const email = extractFromXml(xml, 'sf:Email');
// Lokalen Cache invalidieren
await c.env.KV.delete(`customer:sf:${contactId}`);
// ACK response
return c.text(`<?xml version="1.0"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<notificationsResponse>
<Ack>true</Ack>
</notificationsResponse>
</soapenv:Body>
</soapenv:Envelope>
`, 200, { 'Content-Type': 'text/xml' });
});
Environment Variables¶
# Salesforce Connected App
SF_INSTANCE_URL=https://yourorg.my.salesforce.com
SF_CLIENT_ID=your_client_id
SF_CLIENT_SECRET=your_client_secret