Server-Analyse: STARQstrom Bestandssystem
Datum: 25.02.2026 | Server: 62.108.57.65 (Hetzner) | Zugang: SSH (User: helio)
1. Zusammenfassung
Der Server hostet drei separate Systeme, nicht eine „Middleware“:
| # | System | Domain | Pfad | Tech |
|---|---|---|---|---|
| 1 | Website | starqstrom.de | /var/www/starqstrom-production/ |
Statamic CMS 5.0 + Laravel 12 + Livewire 3.5 + PHP 8.3 |
| 2 | Middleware | middleware.starqstrom.de | /var/www/starqstrom-middleware/production/ |
Pure Laravel 12 + PHP 8.4 + SQLite + Horizon |
| 3 | Wattform API | wechselprozess.services | Extern (SOLVIT) | SOLVIT Backend |
Zusätzlich existieren Staging-Umgebungen für Website und Middleware.
Drei Schlüsselerkenntnisse
- SOLVIT hat bereits eine API („Wattform“) — 7+ Endpoints aktiv in Nutzung
- Die eigene Auftragsstrecke existiert bereits — 7-Schritte Livewire Order Flow ersetzt das SOLVIT-Widget
- Die Middleware speichert KEINE Kundendaten — nur HubSpot-Sync-Metadaten
2. Laufende Services
| Service | Version | Zweck |
|---|---|---|
| nginx | - | Reverse Proxy + SSL (Let’s Encrypt) |
| php8.3-fpm | 8.3 | Website (Statamic) |
| php8.4-fpm | 8.4 | Middleware |
| redis-server | - | Sessions, Cache, Queues |
| supervisor | - | Laravel Horizon (Queue Worker) |
3. Website (starqstrom.de)
Tech-Stack
- CMS: Statamic 5.0 (Flat-File CMS auf Laravel)
- Framework: Laravel 12
- PHP: 8.3
- Frontend: Livewire 3.5, Vite
- HTTP Client: Saloon v3 (für API-Calls)
- Captcha: hCaptcha/Turnstile
- Monitoring: Sentry
- Deployment: Deployer (Release 263, Symlink
current → releases/263)
Custom Packages
digitalmasters/statamic-starterkitv5.3.0 (aus GitLab)studio1902/statamic-peak-seo(SEO)aryehraber/statamic-captcha(Captcha)
Routen
GET /bestellung/{channel?} → Livewire Order Flow (privat/gewerbe)
GET /styleguide → Design-System-Referenz
Livewire Order Flow (7 Steps)
| Step | Klasse | Felder |
|---|---|---|
| 1 | LocationStep |
PLZ, Stadt, Straße, Verbrauch (kWh), Zählertyp (KME/MME/IMS), Rabattcode |
| 2 | TariffSelectionStep |
Tarife von Wattform API, Tarifauswahl |
| 3 | PersonalDataStep |
Anrede, Vorname, Nachname, Email, Telefon, Geburtsdatum, Firma (B2B), Ansprechpartner, alt. Rechnungsadresse |
| 4 | DeliveryAndMeterStep |
Zählernummer, Auftragsgrund (Wechsel/Einzug), Wunsch-Lieferbeginn, Vorversorger |
| 5 | BankDetailsStep |
Zahlungsart (SEPA/Selbstzahler), IBAN, Kontoinhaber (IBAN-Validierung via Wattform) |
| 6 | SummaryStep |
Zusammenfassung aller Daten → Submit an Wattform + Middleware |
| 7 | SuccessStep |
Bestätigungsseite mit Auftragsnummer |
Enums
| Enum | Werte |
|---|---|
CustomerChannel |
privat (B2C), gewerbe (B2B) |
OrderType |
Lieferantenwechsel, Einzug |
SupplyType |
Strom |
MeterType |
KME (Analog), MME (Digital/mME), IMS (Smart/iMSys) |
PaymentMethod |
sepa_lastschrift, selbstzahlung |
DeliveryStartType |
(Wechsel vs. Einzug Timing) |
Query-Parameter (URL-Prefilling)
Die Auftragsstrecke unterstützt URL-Parameter für Vorausfüllung:
/bestellung/privat?postalCode=10115&personCount=2&discountCode=STARQ10
/bestellung/gewerbe?zip=10115&usage=50000&type=gewerbe
Aliases: zip → postalCode, type → channel, usage → manualConsumption
4. Wattform API (SOLVIT)
Konfiguration (aus .env)
WATTFORM_BASE_URL=https://wechselprozess.services/solvreg/resources
WATTFORM_ORIGIN=https://starqstrom.de
WATTFORM_REFERER=https://starqstrom.de/
WATTFORM_DEFAULT_MANDANT=starqstrom
Authentifizierung
- Header
Mandant: starqstrom - Header
Origin: https://starqstrom.de - Header
Referer: https://starqstrom.de/ - Accept: JSON
Endpoints
| # | Endpoint | Method | Parameter | Response | Caching |
|---|---|---|---|---|---|
| 1 | /tarife/{plz}/{city} |
GET | strasse, stromverbrauch, kanal (privat/gewerbe), waermepumpenoption, selbstzahlerMsb, zaehlertyp, rabattcode |
{ strom: [...tarife] } |
Nein (dynamisch) |
| 2 | /auftraege |
POST | OrderData (JSON, siehe unten) |
{ auftragsnummer: "..." } oder { created: timestamp } |
N/A |
| 3 | /postleitzahlen/{plz} |
GET | PLZ | Array von Orten | 1h Cache |
| 4 | /strassen/{plz}/{city} |
GET | Versorgungsart | Array von Straßen | 1h Cache |
| 5 | Rabattcode-Endpoint | POST | discountCode |
true / false (plain text) |
Nein |
| 6 | IBAN-Endpoint | POST | iban, accountHolder |
{ gueltig: true/false } |
Nein |
| 7 | Versorger-Endpoint | POST | query, supplyType |
Array von Versorgern | Nein |
OrderData (POST /auftraege) — Komplette Datenstruktur
{
"stammdaten": {
"anrede": "Herr",
"titel": null,
"vorname": "Max",
"nachname": "Mustermann",
"geburtsdatum": "1990-01-15",
"email": "max@example.de",
"telefon": "030-12345678",
"mobile": null,
"firma": null,
"ansprechpartner": null,
"fax": null,
"starqstromKundennummer": null
},
"bankverbindung": {
"kontoinhaber": "Max Mustermann",
"kontonummer": null,
"bankleitzahl": null,
"bankinstitut": null,
"bic": null,
"iban": "DE89370400440532013000",
"einzugErlaubt": true
},
"lieferadresse": {
"anrede": "Herr",
"titel": null,
"vorname": "Max",
"nachname": "Mustermann",
"land": "DE",
"ort": "Berlin",
"postfach": null,
"postleitzahl": "10115",
"strasse": "Musterstraße",
"zusatz": null,
"hausnummer": "42"
},
"rechnungsadresse": { ... },
"auftraege": {
"strom": [{
"art": "Lieferantenwechsel",
"lieferbeginn": {
"art": "Wunschtermin",
"datum": "2026-04-01"
},
"geraet": {
"zaehlernummer": "1EMH0012345678",
"zaehlertyp": "KME"
},
"vorversorger": {
"name": "Vattenfall",
"kundennummer": "V-123456"
},
"tarif": {
"tarifName": "STARQ&fair",
"referenzbezeichner": "...",
"zaehlertyp": "KME",
"tarifTyp": "AllInklusive",
"mindestvertragslaufzeit": 12,
"mindestvertragslaufzeitEinheit": "Monate",
"jahresverbrauch": 2500,
"arbeitspreis": 0.2890,
"grundpreis": 9.95,
"arbeitspreisBrutto": 0.3439,
"grundpreisBrutto": 11.84
},
"objekttyp": null,
"organisationstyp": null,
"nettopreiseKommuniziert": false
}]
},
"werbungOk": false,
"rabattcode": null,
"werbenderKunde": null,
"kundennummer": null
}
Tarif-Response (GET /tarife) — Wichtige Felder
tarifName— z.B. „STARQ&fair“referenzbezeichner— interner Identifierzaehlertyp— KME/MME/IMStarifTyp— „AllInklusive“mindestvertragslaufzeit+mindestvertragslaufzeitEinheitjahresverbrauch— angenommener Verbraucharbeitspreis(netto),arbeitspreisBruttogrundpreis(netto/Monat),grundpreisBruttobruttoJahresverbrauchspreis,nettoJahresverbrauchspreisabschlag(brutto/Monat),abschlagNetto
5. Middleware (middleware.starqstrom.de)
Tech-Stack
- Framework: Laravel 12 (kein Statamic, pures Laravel!)
- PHP: 8.4
- DB: SQLite
- Queue: Redis + Laravel Horizon
- HubSpot:
hubspot/api-clientv13.1 +digitalmasters/hubspot-starterkit - Monitoring: Sentry
- Version: 1.4.0
- Deployment: Deployer (Release 24)
Konfiguration (aus .env)
MIDDLEWARE_BASE_URL=https://middleware.starqstrom.de
HUBSPOT_ACCESS_TOKEN=pat-eu1-030dffea-...
HUBSPOT_PORTAL_ID=146570641
HUBSPOT_BASE_CONTACT_FORM_GUID=19cc30f7-fb65-4798-883e-99d8e4983131
HUBSPOT_API_TOKEN=40415d7d... (für eingehende Requests von Website)
HUBSPOT_B2B_PIPELINE_ID=2784063729
HUBSPOT_NEW_LEAD_STAGE_ID=3818694860
API-Endpunkte
| Endpoint | Method | Auth | Zweck |
|---|---|---|---|
POST /api/hubspot/orders/submit |
POST | X-HubSpot-API-Key Header |
SLP-Bestellung empfangen → async ProcessOrderJob |
POST /api/hubspot/rlm-leads/submit |
POST (multipart) | X-HubSpot-API-Key Header |
RLM-Lead + Dateianhänge → sync Verarbeitung |
GET / |
GET | - | Health Check (name, status, version, environment) |
Datenbank-Schema (SQLite)
contacts:
| Feld | Typ | Zweck |
|---|---|---|
| id | INTEGER | Primary Key |
| hashed_id | VARCHAR | MD5 von Email (Deduplizierung) |
| external_id | VARCHAR | Externe Referenz |
| source | VARCHAR | „Website“ |
| VARCHAR | Einziges PII-Feld! | |
| company_id | INTEGER | FK zu companies |
| hubspot_id | VARCHAR | HubSpot Object ID |
| sync_status | VARCHAR | pending/syncing/synced/failed |
| synced_at | DATETIME | Letzter erfolgreicher Sync |
| sync_attempts | INTEGER | Anzahl Versuche |
| sync_error | TEXT | Letzte Fehlermeldung |
companies:
id, hashed_id, external_id, source, hubspot_id, sync_status, synced_at, sync_attempts, sync_error
deals:
id, hashed_id, external_id, source, title, contact_id, company_id, hubspot_id, sync_status, synced_at, sync_attempts, sync_error
Order Processing Flow
POST /api/hubspot/orders/submit
↓ Validierung (orderReference, customerChannel, notificationData)
↓ Dispatch ProcessOrderJob (async, Redis Queue "orders")
↓
ProcessOrderJob:
↓ OrderDataMapper.mapOrderData() → { contact, company, deal }
↓
└&horz;&horz; DirectSyncService.syncContact()
│ └&horz;&horz; Prüfe ob Contact existiert (hashed_id = MD5(email))
│ └&horz;&horz; Marketing Consent? → Forms API (GDPR/DOI)
│ └&horz;&horz; Kein Consent? → Contacts API (Upsert by email)
│
└&horz;&horz; DirectSyncService.syncCompany() [nur B2B]
│ └&horz;&horz; hashed_id = MD5(company_name) ← PROBLEM: Kein Upsert!
│ └&horz;&horz; Immer CREATE → Company-Duplikate in HubSpot
│
└&horz;&horz; DirectSyncService.syncDeal()
│ └&horz;&horz; hashed_id = MD5(deal_title)
│ └&horz;&horz; Upsert + 30+ Properties
│
└&horz;&horz; Associations erstellen
└&horz;&horz; Contact ↔ Company (Type 1)
└&horz;&horz; Deal ↔ Contact (Type 3)
└&horz;&horz; Deal ↔ Company (Type 5)
HubSpot Property Mappings
Contact → HubSpot
| Internes Feld | HubSpot Property | Wert/Mapping |
|---|---|---|
| salutation | salutation | Herr/Frau/Firma |
| firstname | firstname | |
| lastname | lastname | |
| Unique Identifier | ||
| phone | phone | |
| street + street_nr | address | Formatiert: „Musterstr. 42“ |
| postal_code | zip | |
| city | city | |
| country | country | „Deutschland“ |
| lifecycle_stage | lifecyclestage | „opportunity“ (statisch) |
| lead_status | hs_lead_status | „Qualifiziert“ (statisch) |
| - | kontaktquelle | „Website“ (Custom) |
Deal → HubSpot (30+ Properties)
| Internes Feld | HubSpot Property | Wert/Mapping |
|---|---|---|
| title | dealname | „{Tarif} - {Kundenname}“ |
| pipeline | pipeline | B2B: 2784063729, B2C: default |
| dealstage | dealstage | B2B: 3818694864, B2C: 2743940327 („Auftrag gewonnen“) |
| amount | amount | gesamtpreisBrutto oder abschlag×12 |
| closedate | closedate | Lieferbeginn-Datum |
| delivery_start | delivery_start | Lieferbeginn |
| discount_approval | discount_approval | Rabattcode |
| - | jahresverbrauch__kwh_ | kWh/Jahr |
| - | netto_arbeitspreis | ct/kWh netto |
| - | grundpreis | EUR/Monat netto |
| - | tarif | Tarifname |
| - | mindestvertragslaufzeit_in_monaten | Monate |
| - | zahlernummer | Zählernummer |
| - | zahlerart | „slp“ (statisch) |
| - | zahlertyp | KME→„Analog“, MME→„Digital“, IMS→„Smart“ |
| - | zahlungsart | sepalastschrift / selbstzahlung_uberweisung |
| - | kontoinhaber | |
| - | iban | |
| - | lieferadresse_strae | Straße |
| - | lieferadresse_hausnr | Hausnummer |
| - | lieferadresse_hausnummernzusatz | |
| - | lieferadresse_plz | PLZ |
| - | lieferadresse_stadt | Stadt |
| - | auftragsgrund | Wechsel / Einzug |
| - | rechnungsadresse__stra_e_ | |
| - | rechnungsadresse__hausnr__ | |
| - | rechnungsadresse__hausnummernzusatz_ | |
| - | rechnungsadresse__plz_ | |
| - | rechnungsadresse__stadt_ | |
| - | kontaktquelle | „Website“ |
| - | kundentyp |
Company → HubSpot
| Internes Feld | HubSpot Property |
|---|---|
| name | name |
| email → domain | domain |
| e_mail_company | |
| phone | phone |
| street + street_nr | address |
| postal_code | zip |
| city | city |
| country | country |
6. Bekannte Probleme
Company-Duplikate (bestätigt im Code)
// DirectSyncService.php:
// NOTE: We always create a new company instead of upserting by domain.
// Reason: The 'domain' property is not configured as unique in HubSpot
// TODO: Consider configuring 'domain' as unique in HubSpot to enable deduplication.
Impact: Jede B2B-Bestellung erstellt eine neue Company in HubSpot.
Lösung für STARQ Platform: Domain als unique Property in HubSpot konfigurieren + E-Mail-Domain-basiertes Upsert.
Fehlende Rückmeldung von SOLVIT
Die Wattform API hat kein Webhook/Callback-System. Die Website schickt Bestellungen ab, bekommt eine auftragsnummer zurück — aber danach gibt es keine Statusupdates (Klärfälle, Ablehnungen, Vertragsstatus).
Kein RLM über Wattform
RLM-Leads werden nur an die Middleware geschickt (mit Dateianhängen), NICHT an die Wattform API. Der B2B-Flow geht direkt in HubSpot — es gibt keinen automatisierten SOLVIT-Weg für RLM.
7. Dateipfade (Referenz)
Website (/var/www/starqstrom-production/current/)
app/
Http/Integrations/
Wattform/
WattformConnector.php ← Saloon HTTP Connector
Requests/
GetTariffs.php ← GET /tarife/{plz}/{city}
SubmitOrder.php ← POST /auftraege
GetPostalCodes.php ← PLZ-Validierung
GetStreets.php ← Straßen-Lookup
ValidateDiscountCode.php ← Rabattcode prüfen
ValidateIban.php ← IBAN validieren
SearchSupplier.php ← Vorversorger suchen
DataTransferObjects/
OrderData.php ← Komplette Bestelldaten
TariffData.php ← Tarif-Struktur
TariffRequestData.php ← Tarif-Abfrage-Parameter
StammdatenData.php ← Persönliche Daten
BankverbindungData.php ← SEPA-Daten
StromAuftragData.php ← Strom-Auftrag
AddressData.php ← Adresse
LieferbeginnData.php ← Lieferbeginn
GeraetData.php ← Zähler-Daten
VorversorgerData.php ← Vorversorger
Middleware/
MiddlewareConnector.php ← HTTP Connector zur Middleware
Requests/
SubmitOrderToMiddleware.php ← POST /api/hubspot/orders/submit
SubmitRlmLeadToMiddleware.php ← POST /api/hubspot/rlm-leads/submit
Livewire/OrderFlow/
OrderFlowController.php ← Haupt-Controller (Session, Steps, Navigation)
LocationStep.php ← Step 1: Standort
TariffSelectionStep.php ← Step 2: Tarifauswahl
PersonalDataStep.php ← Step 3: Persönliche Daten
DeliveryAndMeterStep.php ← Step 4: Zähler & Lieferbeginn
BankDetailsStep.php ← Step 5: Bankverbindung
SummaryStep.php ← Step 6: Zusammenfassung & Submit
SuccessStep.php ← Step 7: Bestätigung
Services/
OrderService.php ← Wattform API Orchestrierung
ProductTariffMatcher.php ← Tarif-Matching
Enums/
CustomerChannel.php ← privat/gewerbe
OrderType.php ← Lieferantenwechsel/Einzug
MeterType.php ← KME/MME/IMS
PaymentMethod.php ← SEPA/Selbstzahlung
SupplyType.php ← Strom
Middleware (/var/www/starqstrom-middleware/production/current/)
app/
Http/Controllers/Api/HubSpot/
OrderController.php ← POST /api/hubspot/orders/submit
RlmLeadsController.php ← POST /api/hubspot/rlm-leads/submit
Jobs/
ProcessOrderJob.php ← Async Order-Verarbeitung
SyncContactToHubSpotJob.php
SyncCompanyToHubSpotJob.php
SyncDealToHubSpotJob.php
Models/
Contact.php ← Sync-Metadaten
Company.php ← Sync-Metadaten
Deal.php ← Sync-Metadaten
Services/HubSpot/
DirectSyncService.php ← Direkter HubSpot-Sync (kein Queue)
HubSpotFormsService.php ← Forms API (für Marketing Consent / GDPR)
HubSpotFileUploadService.php ← Datei-Upload nach HubSpot
Mappers/
OrderDataMapper.php ← Order → Contact + Company + Deal
ContactFieldMapper.php ← Internes Format → HubSpot Properties
DealFieldMapper.php ← Internes Format → HubSpot Properties
CompanyFieldMapper.php ← Internes Format → HubSpot Properties
RlmLeadDataMapper.php ← RLM-Lead Mapping
RlmLeadService.php ← RLM-Lead Verarbeitung
Services/Order/
OrderProcessingService.php ← Order Pipeline
8. Implikationen für STARQ Platform (Rails)
Was muss portiert werden
- Wattform API Client — Saloon Connector → Ruby HTTP Client (Faraday/HTTParty)
- HubSpot Sync — DirectSyncService + Mappers → Rails Service Objects
- Order Processing — ProcessOrderJob → Sidekiq Job
- Contact/Company/Deal Models — SQLite → PostgreSQL
- Field Mappings — 30+ Deal-Properties, Contact, Company (exakte Property-Names!)
Was NICHT portiert werden muss
- Statamic CMS — Website bleibt bestehen, sendet nur an neue API
- Livewire Order Flow — bleibt in Statamic, nur API-Ziel ändert sich
- Deployer — wird durch Kamal ersetzt
Neue Features (nicht im Bestandssystem)
- SOLVIT Proxy — Wattform-Calls über STARQ Platform (Transparenz, Logging)
- Company Dedup — Domain als unique + Fuzzy-Matching
- Webhook-System — Statusupdates von SOLVIT empfangen
- RLM über Wattform — Automatisierter B2B-Flow
- Partner Portal — Dashboard, Provisionen
- Pricing Engine (Phase 3) — Eigene Preisberechnung