Solidus in eine Full Stack Plattform für Autohandel und Vermietung verwandeln
Kurz gesagt
Niveau: Profi
- Ein Gebrauchtwagen ist in Solidus ein Produkt mit einer Variante und einer Menge von genau eins in seiner Filiale, Nachbestellungen aus. Jede Filiale ist ein Lagerort.
- Tatsachen über das Auto sind Eigenschaften, Wahlmöglichkeiten sind Optionstypen, und alles, wonach du per Zahl filterst (Kilometerstand), braucht eine echte Spalte über ein Override.
- Mietverfügbarkeit ist ein Kalender, keine Bestandszahl: Ein daterange in PostgreSQL mit Exclusion Constraint verhindert Doppelbuchungen in der Datenbank selbst.
- Mietpreise kommen aus einem eigenen Price Selector, der die Daten liest, und die Kaution ist eine Kartenautorisierung, die bei der Rückgabe teilweise eingezogen oder storniert wird.
- Deutsche Regeln, auf die du baust: 14 Tage Widerruf bei Autos aus dem Fernabsatz, kein Widerruf bei Mieten mit festem Termin, die Halterpflicht beim Führerschein und die Differenzbesteuerung.
Ein Auto ist ein Einzelstück, kostet Tausende, wird mit Anzahlung und Finanzierung bezahlt und, wenn du es vermietest, nach Datum gebucht statt aus dem Regal verkauft. Dieser Artikel zeigt Schritt für Schritt, wie jede dieser sperrigen Tatsachen auf Solidus passt, an einem erfundenen Autohaus mit Filialen in Hamburg und Köln, und verlinkt die echte Autohaus Plattform, die ich darauf gebaut habe.
Ein Auto passt schlecht in einen Onlineshop
Die meisten Onlineshops verkaufen Dinge im Karton. Zwanzig gleiche Tassen stehen im Regal, jemand kauft zwei, 18 bleiben übrig, ein Paket geht raus. Jede eCommerce Plattform ist um dieses Bild herum gebaut.
Ein Auto bricht fast jeden Teil davon. Ein Gebrauchtwagen ist ein Einzelstück. Er kostet Tausende Euro, also zahlen die Leute zuerst eine Anzahlung, und viele finanzieren. Er steht in einer bestimmten Filiale und wird dort abgeholt oder zum Käufer gefahren. Vermietet derselbe Betrieb auch Autos, lautet die Frage nicht mehr "wie viele sind auf Lager", sondern "ist dieses Auto von Freitag bis Montag frei". Und um jeden Verkauf und jede Miete herum liegt Papier: die Führerscheinprüfung, der Vertrag, die Zulassungsbescheinigung.
Solidus ist eine Open Source eCommerce Plattform, geschrieben in Ruby on Rails.1 Sie passt zu so einem Betrieb, weil sich fast jedes Teil davon in schlichtem Ruby ersetzen oder erweitern lässt. Im Folgenden lege ich jedes sperrige Stück eines Autohauses der Reihe nach auf Solidus um, und am Ende kommen die deutschen Regeln, die den Bau prägen.
Ich habe das wirklich gebaut. Die Fallstudie zur Autohaus Plattform beschreibt es: eine Plattform auf Ruby on Rails und Solidus, live hinter Auto Prammer, auf der Verkauf, Vermietung, Werkstatt, Gutscheine und Marketing alle aus einem Admin laufen. Alles hier unten ist die allgemeine Methode, erklärt an einem erfundenen Autohaus, nicht das Setup des Kunden.
Beispiel: ein erfundenes Autohaus in Hamburg
Das Autohaus Elbufer ist erfunden. Katrin führt es mit zwei Filialen, eine in Hamburg und eine in Köln. Sie verkauft Gebrauchtwagen und vermietet eine kleine Flotte Kompaktwagen, und ihr Shop liegt unter autohauselbufer.example.
Drei Aufgaben müssen auf einem System laufen:
- Gebrauchtwagen verkaufen, jeder ein Einzelstück, zu Preisen zwischen etwa 8.000 € und 25.000 €.
- Flottenautos tageweise vermieten, abgeholt und zurückgegeben in einer Filiale.
- Bei jeder Miete eine Kaution blockieren und sie ganz oder teilweise freigeben, wenn das Auto zurückkommt.
Ein Gebrauchtwagen ist ein Produkt mit genau einer Einheit
Solidus beschreibt, was du verkaufst, in zwei Ebenen. Ein Produkt ist das, was auf der Seite steht: Name, Beschreibung, Bilder. Eine Variante ist die Ausführung, die jemand wirklich kaufen kann, mit eigenem Preis, eigenem Bestand und eigener SKU (Stock Keeping Unit, die interne Artikelnummer). Jedes Produkt hat eine Mastervariante und kann weitere Varianten haben, wenn es etwas zu wählen gibt.2
Der Bestand sitzt eine Ebene tiefer. Für jede Variante und jeden Lagerort gibt es ein Stock Item, und das Stock Item hält die vorhandene Menge. Solidus lehnt eine negative Menge ab, außer das Stock Item ist als nachbestellbar (backorderable) markiert, und es ändert die Menge unter einer Datenbanksperre, damit nicht zwei Checkouts gleichzeitig die letzte Einheit nehmen.3 Ist die Bestandsführung aktiv, gilt eine Variante für Solidus nur dann als vorrätig, wenn eines ihrer Stock Items eine Menge über null hat.4
Für einen Gebrauchtwagen heißt das: ein Produkt pro Auto, eine Mastervariante, eine Menge von genau eins in der Filiale, in der das Auto steht, und Nachbestellungen aus. Der Verkauf setzt die Menge auf null, und das Auto verschwindet aus allem, was verfügbaren Bestand listet.
Beispiel: der blaue Golf in Hamburg
Katrin kauft einen Volkswagen Golf von 2021 und stellt ihn in Hamburg auf den Hof.
- Sie legt ein Produkt an, "2021 Volkswagen Golf Life 1.5 TSI, blau", zum Preis von 16.495 €.
- Solidus legt dazu die Mastervariante an. Sie bekommt die SKU
EB-0412. - Am Lagerort Hamburg bekommt das Stock Item eine Menge von 1, nachbestellbar aus. In Köln ist die Menge 0.
- Eine Käuferin zahlt die Anzahlung online. Der Checkout schließt ab, und die Menge in Hamburg geht von 1 auf 0.
- Ein zweiter Käufer öffnet eine Minute später dieselbe Seite. Die Variante hat jetzt kein Stock Item mehr über null, das Auto gilt also nicht mehr als vorrätig, und für eine zweite Bestellung ist keine Einheit mehr übrig.
Dafür war kein eigener Code nötig. Das Bestandsmodell versteht "eins von eins" schon, solange Nachbestellungen aus bleiben.
Eigenschaften beschreiben das Auto, Optionstypen beschreiben eine Wahl
Ein Optionstyp (Option Type) ist eine Wahl, die der Kunde trifft und die eigene Varianten erzeugt. Der Solidus Quellcode beschreibt das mit dem klassischen Beispiel: ein Optionstyp wie Größe, mit Werten wie klein, mittel und groß.5 Jede Kombination von Werten wird zu einer eigenen Variante, mit eigenem Preis und eigenem Bestand.
Eine Eigenschaft (Property) ist eine schlichte Tatsache über das Produkt: ein Name und ein Wert, etwa "Kraftstoff: Benzin". Sie erzeugt nichts. Sie wird auf der Seite angezeigt. Außerdem sitzen Produkte in Taxons, also Kategorien wie "Marke, dann Volkswagen" oder "Karosserie, dann Kompaktwagen".2
Bei einem Gebrauchtwagen gibt es meistens nichts zu wählen, also auch keine Optionstypen. Kilometerstand, Kraftstoff, Getriebe und Farbe sind Eigenschaften. Marke und Karosserie sind Taxons, und daraus bekommt der Shop seine Kategorieseiten und Filter.
Bei der Vermietung verdienen sich Optionstypen ihren Platz. Niemand mietet "den Yaris mit Flottennummer 7". Man mietet "einen kleinen Automatik", und man wählt, wie viele Kilometer man fahren will.
Beispiel: der kleine Automatik zur Miete
Katrin richtet ihr Mietangebot so ein (das Beispiel rechnet in Kilometern):
- Ein Produkt, "Kompaktwagen mit Automatik", im Taxon "Miete, dann Klein".
- Ein Optionstyp, "Kilometer", mit zwei Werten: "161 km am Tag" und "Unbegrenzt".
- Das ergibt zwei Varianten. Die Variante mit 161 km hat einen Grundpreis von 42 € am Tag. Die unbegrenzte Variante hat einen Grundpreis von 51 € am Tag.
- Eigenschaften am Produkt: "Sitze: 5", "Getriebe: Automatik", "Kraftstoff: Hybrid".
Sobald der Kunde einen Kilometerwert wählt, weiß Solidus, welche Variante und welcher Grundpreis gilt. Die Flottenautos dahinter leben im Mietkalender.
Fahrzeugdaten, nach denen du filterst, brauchen echte Spalten
Eigenschaften haben eine Schwäche. Ihre Werte sind Text. Für "Kraftstoff: Benzin" ist das in Ordnung. Schief geht es, sobald du nach einer Zahl sortierst oder filterst.
Text wird Zeichen für Zeichen verglichen, von links. Der Text "83.686" gilt deshalb als größer als "100.000", weil das Zeichen 8 nach dem Zeichen 1 kommt. Ein Filter "unter 100.000 km", der auf dem Text der Eigenschaft aufbaut, würde ein Auto mit 83.686 km ausblenden, obwohl es genau in den Filter gehört.
Die Lösung: Die Zahlen, nach denen du filterst, bekommen echte Datenbankspalten. In Solidus ist der empfohlene Weg, eine Kernklasse wie das Produkt zu ändern, ein Override: ein kleines Modul, das du der Klasse voranstellst (prepend), damit dein Code zuerst läuft und etwas ergänzen kann.6 Die Fahrzeugdaten kommen in eine eigene Tabelle und werden über ein Override an das Produkt gehängt. Die Kerntabellen bleiben unberührt, und Updates bleiben einfacher.
Beispiel: eine Fahrzeugtabelle für das Autohaus Elbufer
Die Migration legt eine Zeile pro Auto an, verknüpft mit seinem Produkt:
create_table :elbufer_vehicles do |t| # eine Zeile pro Fahrzeug, genau eine pro Produkt
t.references :spree_product, null: false, foreign_key: true, index: { unique: true }
t.string :vin, null: false
t.date :first_registered_on, null: false
t.integer :mileage, null: false # Kilometerstand in km
t.string :fuel, null: false
t.string :transmission, null: false
t.timestamps
end
Das Override gibt jedem Produkt sein Fahrzeug:
module Elbufer
module Spree
module Product
module AddVehicle
def self.prepended(base)
base.has_one :vehicle, class_name: "Elbufer::Vehicle", foreign_key: :spree_product_id
end
::Spree::Product.prepend self
end
end
end
end
Jetzt ist der Filter ein Zahlenvergleich:
Spree::Product.joins(:vehicle).where(elbufer_vehicles: { mileage: ..100_000 }) # alle Autos unter 100.000 km
Nimm drei Autos mit 15.772, 50.212 und 83.686 km. Als Text behält ein Filter "unter 100.000" kein einziges: "50.212" und "83.686" beginnen mit einem Zeichen nach der 1, und bei "15.772" entscheidet das zweite Zeichen, denn die 5 kommt nach der 0. Als ganze Zahlen behält er alle drei, und das ist richtig. Die passenden Autos tauchen nur mit der echten Spalte auf.
Jede Filiale ist ein Lagerort
Ein Lagerort (Stock Location) ist in Solidus ein benannter Ort, von dem aus Ware geliefert wird, mit eigener Adresse. Jeder hat seine eigenen Stock Items, und Versandarten sind mit Lagerorten verknüpft.7 Beim Autohaus Elbufer sind die beiden Filialen die beiden Lagerorte.
Wenn eine Bestellung in den Lieferschritt geht, startet Solidus einen Stock Coordinator. Er filtert die Lagerorte (standardmäßig bleiben nur die aktiven), sortiert sie, teilt die Einheiten zu, baut Pakete und berechnet dann für jedes Paket die Versandkosten aus den verfügbaren Versandarten.8 Für ein Autohaus heißt "Versandart" vor allem: Wie kommt der Käufer an sein Auto. Abholung in der Filiale oder Zustellung durch einen Fahrer.
Beispiel: ein Käufer in Köln will den Golf aus Hamburg
Bevor der Verkauf durch ist, fragt ein Käufer in Köln nach dem blauen Golf.
- Das Stock Item des Golfs in Hamburg hält 1. In Köln hält es 0.
- Katrin bietet zwei Versandarten an. "Abholung in der Filiale" kostet 0 € und ist mit beiden Filialen verknüpft. "Zustellung durch unseren Fahrer im Umkreis von 80 km" kostet 149 € und ist ebenfalls mit beiden verknüpft. Beide Preise sind Annahmen dieses Beispiels.
- Der Käufer holt lieber in Köln ab. Katrins Team überführt das Auto.
- Im Admin senken sie die Menge in Hamburg um eins und erhöhen die in Köln um eins. Hamburg zeigt jetzt 0, Köln zeigt 1.
- Der Käufer schließt den Checkout ab. Der Coordinator findet die Einheit in Köln, und "Abholung in der Filiale" kostet 0 €.
Bestellung, Bestand und Abholort stimmen jetzt überein, ohne Tabelle, in der steht, welches Auto wo steht.
Mietverfügbarkeit ist ein Kalender, keine Bestandszahl
Eine Bestandszahl beantwortet "wie viele kann ich jetzt verkaufen". Eine Miete muss beantworten "ist genau dieses Auto zwischen zwei Daten frei", und dasselbe Auto ist am Tag seiner Rückgabe wieder frei. Solidus hat keinen Buchungskalender, also ist das der Teil, den du baust.
Der sauberste Ort für die Regel ist die Datenbank selbst. PostgreSQL kennt Bereichstypen (Range Types) wie daterange und Exclusion Constraints. Ein Exclusion Constraint kann sagen "keine zwei Zeilen dürfen sich überlappende Bereiche haben", und mit der Erweiterung btree_gist kann er ergänzen "für dasselbe Auto".9 Rails unterstützt beides in Migrationen: eine Spalte vom Typ daterange und einen exclusion_constraint auf der Tabelle.10
Ein Detail lässt das für Mieten funktionieren. Ein daterange schließt seinen ersten Tag ein und seinen letzten Tag aus. PostgreSQL schreibt das als [).9 Eine Buchung vom 14. bis 17. August blockiert also die Nächte vom 14., 15. und 16., und eine neue Buchung darf am 17. beginnen. Mieten direkt hintereinander sind erlaubt, überlappende werden abgelehnt, und die Ablehnung kommt aus der Datenbank, auch wenn zwei Leute in derselben Sekunde auf "Buchen" drücken.
Der Ablauf führt von der Wahl des Kunden zu einem von zwei Ergebnissen: Die Daten sind auf einem bestimmten Auto reserviert, oder jedes Auto der Klasse ist belegt. Die Entscheidung ist das wichtige Feld. Die Prüfung ist das Einfügen selbst, keine Abfrage vorher, die ein anderer Kunde um eine Sekunde schlagen könnte.
Beispiel: Flottenauto 7 im August
Flottenauto 7 ist ein kleiner Automatik in Hamburg. Die Buchungstabelle hat einen Verweis auf das Auto und ein daterange namens period:
enable_extension "btree_gist"
create_table :elbufer_bookings do |t|
t.references :elbufer_fleet_car, null: false, foreign_key: true
t.daterange :period, null: false
# Keine zwei Buchungen desselben Autos mit überlappendem Zeitraum
t.exclusion_constraint "elbufer_fleet_car_id WITH =, period WITH &&", using: :gist
t.timestamps
end
Drei Kunden wollen es buchen.
- Buchung A, Abholung Freitag, 14. August 2026, Rückgabe Montag, 17. August. Gespeichert als
[2026-08-14, 2026-08-17). Angenommen. - Buchung B, Sonntag, 16. August, bis Dienstag, 18. August. Gespeichert als
[2026-08-16, 2026-08-18). Der 16. liegt in Buchung A, die Bereiche überlappen, und PostgreSQL lehnt die Zeile ab. Das System geht weiter und probiert Flottenauto 8. - Buchung C, Montag, 17. August, bis Mittwoch, 19. August. Gespeichert als
[2026-08-17, 2026-08-19). Buchung A schließt den 17. aus, also keine Überlappung. Angenommen, und Auto 7 geht direkt wieder raus.
Für einen Reinigungstag zwischen zwei Mieten speicherst du jeden blockierten Zeitraum einen Tag länger als die Miete.
Der Mietpreis ist eine Summe aus Tagespreisen
Ein Mietpreis ist nicht eine Zahl von der Produktseite. Er hängt davon ab, wie viele Tage, welche Tage, welche Saison und ob eine lange Miete einen Rabatt bekommt. Als Formel:
Dabei ist der Mietpreis, die Zahl der Miettage, zählt die Tage von bis , ist der Grundpreis der Variante pro Tag, der Wochenendfaktor für Tag (zum Beispiel an einem Samstag oder Sonntag, sonst ), der Saisonfaktor für Tag (zum Beispiel im August, sonst ) und der Rabatt für die Mietdauer als Anteil, zum Beispiel , wenn sieben oder mehr ist, und darunter . Der Rabatt ist ein Prozentsatz, der von der Summe abgeht, siehe Grundlagen: Prozente.
Wo lebt das in Solidus? Zwei Tatsachen entscheiden es.
Erstens bekommt eine Position im Warenkorb (Line Item) ihren Preis vom Price Selector der Variante, der zu einem Satz Preisoptionen einen Preis auswählt. Standardmäßig sind diese Optionen nur die Währung und das Land.11 Die Preisoptionen einer Position baut eine Klassenmethode namens from_line_item, und sowohl der Selector als auch seine Optionsklasse sind konfigurierbar.1213
Zweitens kann der Browser keinen Preis schicken. Die standardmäßig erlaubten Attribute einer Position sind die Id, die Variante, die Menge und ein Hash namens customer_metadata, sonst nichts.14 Und für Shops mit den Standardwerten von Solidus 5.0 ist eine Einstellung namens recalculate_cart_prices aktiv, die die Warenkorbpreise bei jeder Neuberechnung der Bestellung aus dem Price Selector neu berechnet.13 Ein Preis, den du von Hand auf die Position schreibst, würde überschrieben.
Also kommen die Mietdaten in die customer_metadata der Position. Eine eigene Preisoptionsklasse liest sie in from_line_item. Ein eigener Price Selector wendet die Formel an und gibt den Preis zurück. Weil der Preis aus dem Selector kommt, liefert jede Neuberechnung dasselbe Ergebnis. Eine weitere Einstellung hält zwei Mieten derselben Variante zu verschiedenen Daten auseinander: Solidus erlaubt Vergleichshooks, Methoden an der Bestellung, die entscheiden, ob ein neuer Artikel dieselbe Position ist wie eine bestehende.13
Beispiel: sieben Tage im August
Ein Kunde bucht den kleinen Automatik mit 161 km am Tag, von Freitag, 14. August, bis Freitag, 21. August 2026. Das sind Tage: Freitag, Samstag, Sonntag, Montag, Dienstag, Mittwoch, Donnerstag. Der Grundpreis ist €. Alle sieben Tage liegen im August, also gilt jeden Tag . Samstag und Sonntag haben . Diese Faktoren sind Annahmen dieses Beispiels, gewählt, um die Rechnung zu zeigen.
- Der Preis an einem Wochentag: .
- Der Preis an einem Wochenendtag: .
- Die sieben Tage addieren, fünf Wochentage und zwei Wochenendtage: .
- Der Rabatt für die Dauer. Sieben Tage ergeben , also .
Der Kunde zahlt 340,20 €. In diesem Beispiel sind die Preise inklusive Umsatzsteuer. Zum Vergleich: Das Wochenende von Freitag bis Montag kostet , ohne Rabatt, weil drei unter sieben liegt.
Die waagerechte Achse zeigt die sieben Miettage, die senkrechte den Preis pro Tag in Euro, bevor die 10 % Rabatt von der Summe abgehen. Die Erkenntnis: An den Wochenendtagen wird das Geld verdient, und die sieben Balken zusammen ergeben die 378,00 €, die der Rabatt dann auf 340,20 € senkt.
Eine Kaution ist eine Autorisierung, die später eingezogen wird
Kartenzahlungen haben zwei Schritte. Die Autorisierung reserviert das Geld auf der Karte. Das Einziehen (Capture) nimmt es wirklich. Solidus ist um diese Trennung gebaut: Seine Konfiguration hält fest, dass Autorisieren und späteres Einziehen Fehler viel besser abfängt, und auto_capture ist standardmäßig aus.13 Wenn die Zahlungen einer Bestellung verarbeitet werden, wird jede je nach Zahlungsart entweder autorisiert oder in einem Schritt autorisiert und eingezogen. Eine autorisierte Zahlung wird später eingezogen oder storniert (void), wenn sie nicht gebraucht wird.15 Die Methode capture! nimmt einen Betrag in Cent, du kannst also weniger einziehen, als du autorisiert hast.16
Stripe arbeitet genauso, und ein teilweises Einziehen gibt den Rest automatisch frei. Es gibt eine Grenze: Eine Autorisierung für eine Online Kartenzahlung gilt in der Regel sieben Tage, und eine Zahlung, die nicht rechtzeitig eingezogen wird, wird storniert und das Geld freigegeben.17 Reservierungen, die vor Ort an einem Kartenterminal entstehen, sind kürzer, bei den meisten Kartenmarken zwei Tage.17 Für eine Miete entscheidet dieses Fenster, wann du die Reservierung setzt: kurz vor der Abholung bei kurzen Mieten, und bei langen die verlängerte Autorisierung von Stripe oder ein anderer Weg für die Kaution.
Der Mietpreis ist eine normale Zahlung, eingezogen bei der Buchung. Die Kaution ist eine zweite Zahlung, auf einer Zahlungsart mit auto_capture aus.
Beispiel: die Kaution fürs Wochenende
Eine Kundin mietet den kleinen Automatik übers Wochenende, Freitag, 14., bis Montag, 17. August. Katrins Kaution beträgt 250 €, eine Annahme dieses Beispiels.
- Bei der Buchung zahlt die Kundin den Mietpreis von 176,40 €. Er wird sofort eingezogen.
- Am Freitagmorgen bittet die Mail zur Abholung die Kundin, 250 € online auf derselben Karte zu autorisieren. In Solidus liegt das als ausstehende Zahlung. Vom Konto der Kundin ist noch nichts abgegangen. Die Reservierung gilt sieben Tage, und das Auto kommt nach drei zurück.
- Am Montag kommt das Auto mit einem Viertel Tank zu wenig zurück. Katrins Gebühr fürs Nachtanken beträgt 38 €, wieder eine Annahme.
- Ihr Team zieht 38 € der 250 € ein. In Cent ist das
capture!(3800). - Stripe gibt die übrigen 212 € an die Kundin frei: 250 € minus 38 €.
- Wäre das Auto voll getankt und ohne Schaden zurückgekommen, hätte das Team die Autorisierung storniert und die ganzen 250 € wären freigegeben worden.
Die Kundin zahlt immer nur, was wirklich passiert ist, und niemand muss eine Erstattung bearbeiten.
Ein Checkout für Autos braucht andere Schritte
Ab Werk läuft ein Solidus Checkout so: Warenkorb, Adresse, Lieferung, Zahlung falls nötig, Bestätigung, fertig. Du änderst das mit einem Override an der Bestellung, mit remove_checkout_step und insert_checkout_step und einer Position before oder after.18 Ein Schritt kann auch eine Bedingung tragen, dann gilt er nur für manche Bestellungen.19
Die Solidus Guides enthalten zwei Warnungen, die du kennen solltest, bevor du etwas entfernst. Nimmst du den Adressschritt heraus und behältst die Lieferung, bricht die Lieferung, weil sie eine Adresse erwartet. Und der Stock Coordinator von oben läuft, wenn die Bestellung in die Lieferung geht. Entfernst du diesen Schritt, musst du ihn selbst aufrufen.188
Für das Autohaus Elbufer braucht der Checkout für Mieten etwas, das ein Paketcheckout nicht braucht: eine Führerscheinprüfung vor der Zahlung.
Beispiel: ein Führerscheinschritt nur für Mieten
Das Override fügt einen Schritt hinzu, nur für Bestellungen mit einer Miete:
module Elbufer
module Spree
module Order
module AddLicenceStep
def self.prepended(base)
# Der Schritt erscheint nur, wenn die Bestellung eine Miete enthält
base.insert_checkout_step :licence, before: :payment, if: ->(order) { order.rental? }
end
::Spree::Order.prepend self
end
end
end
end
Die Methode rental? gehört dem Autohaus Elbufer selbst: wahr, wenn eine Position zum Taxon für Mieten gehört. Eine Miete läuft jetzt so: Warenkorb, Adresse, Lieferung, Führerschein, Zahlung, Bestätigung, fertig. Ein Gebrauchtwagenverkauf überspringt den Führerscheinschritt und läuft wie bisher. Der Schritt braucht eine eigene View, ein Formular für die Führerscheindaten, und der Solidus Guide behandelt sie direkt neben dem Code.18
Dokumente gehören an den Datensatz, den sie belegen
Ein Autohaus erzeugt bei jedem Schritt Papier: die Führerscheinprüfung bei einer Miete, den Mietvertrag, die Verkaufsrechnung, Fotos des Autos bei Abholung und Rückgabe. Rails hat dafür Active Storage. Es hängt Dateien mit has_one_attached oder has_many_attached an jeden Datensatz und speichert sie auf der Festplatte oder in einem Cloud Bucket.20
Für die Führerscheinprüfung selbst gibt es in Deutschland keinen staatlichen Onlinedienst, über den ein Kunde seine Daten mit der Vermietung teilt. Geprüft wird der Führerschein selbst. Warum das zählt: Nach dem Straßenverkehrsgesetz macht sich der Halter eines Kraftfahrzeugs strafbar, wenn er anordnet oder zulässt, dass jemand das Fahrzeug führt, der die erforderliche Fahrerlaubnis nicht hat oder ein Fahrverbot hat.21 Bei einem Mietwagen ist der Halter die Vermietung.
Speichere nur, was du brauchst: Führerscheinnummer, Klasse, Datum und Ergebnis der Prüfung. Ein Scan des Führerscheins ist ein personenbezogenes Datum, das du schützen und später löschen musst, das Thema von Datenschutz als Stärke.
Beispiel: wann der Führerschein geprüft wird
Ein Kunde bucht am 10. Juli eine Miete mit Abholung am 14. August. Würde die Prüfung nur bei der Buchung stattfinden, lägen zwischen Prüfung und Fahrt fünf Wochen, und in dieser Zeit kann zum Beispiel ein Fahrverbot beginnen. Die Pflicht des Halters gilt für den Moment, in dem jemand fährt.
Also hält der Führerscheinschritt im Checkout nur die Führerscheinnummer, die Klasse und die Einwilligung fest. Dann:
- Sieben Tage vor der Abholung, am 7. August, erinnert eine Mail daran, den Führerschein im Original mitzubringen.
- Bei der Abholung am 14. August vergleicht eine Mitarbeiterin den Führerschein mit der Person und der hinterlegten Nummer und prüft, ob die Klasse B darauf steht.
- Sie markiert die Buchung mit Datum als "Führerschein geprüft", und das Ergebnis wird an der Buchung gespeichert.
- Der unterschriebene Mietvertrag wird als PDF erzeugt und mit
has_one_attached :contractan die Buchung gehängt, zusammen mit den Fotos bei der Übergabe.
Kommt im Oktober ein Streit auf, liegt alles an einem Datensatz.
Die deutschen Regeln, die den Bau prägen
Ein paar deutsche Regeln ändern, wie sich die Software verhalten muss. Kurz gefasst, und keine Rechtsberatung:
Widerruf bei online verkauften Autos. Bei Fernabsatzverträgen hat ein Verbraucher ein Widerrufsrecht, die Frist beträgt 14 Tage.2223 Bei einem Verbrauchsgüterkauf beginnt sie, sobald der Verbraucher die Ware erhalten hat.24 Für ein online verkauftes und geliefertes Auto läuft die Uhr also ab dem Tag, an dem der Käufer das Auto bekommt, und jede Logik für Anzahlung und Zahlung muss eine Erstattung in diesem Fenster zulassen.
Mieten mit festen Daten. Dasselbe Gesetz nimmt Verträge über Kraftfahrzeugvermietung vom Widerrufsrecht aus, wenn der Vertrag für die Erbringung einen spezifischen Termin oder Zeitraum vorsieht.22 Eine Miete vom 14. bis 17. August ist so ein Vertrag. Deine eigenen Stornobedingungen gelten trotzdem, also muss der Shop klar sagen, wie sie lauten.
Umsatzsteuer auf Gebrauchtwagen. Viele Gebrauchtwagen werden mit Differenzbesteuerung verkauft: Steuer wird auf die Marge fällig, nicht auf den vollen Preis. Voraussetzung ist unter anderem, dass für die Lieferung an den Händler keine Umsatzsteuer geschuldet war oder sie selbst schon differenzbesteuert war.25 Die Umsatzsteuer gehört dabei nicht zur Bemessungsgrundlage, und es gilt der allgemeine Steuersatz von 19 %.2526 Marge und Steuer ergeben sich in zwei Schritten:
Dabei ist die Bruttomarge, der Verkaufspreis, der Einkaufspreis und der Anteil der Steuer an einem Betrag, der 19 % Umsatzsteuer schon enthält. Er folgt daraus, dass die Steuer nicht zur Bemessungsgrundlage gehört: Die Bemessungsgrundlage ist , und % davon sind , siehe Grundlagen: Prozente.
Beispiel: die Umsatzsteuer auf den blauen Golf
Katrin hat den Golf für 14.200 € von einem Privatverkäufer gekauft. Für diese Lieferung war keine Umsatzsteuer geschuldet, also passt das Auto in die Differenzbesteuerung. Sie verkauft es für 16.495 €.
- Die Marge: .
- Die Bemessungsgrundlage: .
- Die Steuer: .
- Zur Probe: .
Sie schuldet auf diesen Verkauf 366,43 € Umsatzsteuer, nicht die 2.633,66 € (), die auf den vollen Preis fällig wären. Wer die englische Ausgabe dieses Artikels kennt: Dort steht eine andere Zahl, weil der britische Steuersatz 20 % beträgt und nicht 19 %. Die Steuerberechnung von Solidus arbeitet standardmäßig mit Steuerkategorien, Steuersätzen und Rate Calculators, und der Guide zeigt zwei Wege, sie zu ändern: einen eigenen Rate Calculator oder einen ganz eigenen Tax Calculator.27 Ein differenzbesteuertes Auto braucht einen davon, gefüttert mit dem Einkaufspreis aus dem Fahrzeugdatensatz, oder die Steuer wird außerhalb der Bestellung berechnet. Entscheide das, bevor das erste Auto live geht.
Wo du anfängst
Jedes Teil ist eine kleine Erweiterung desselben Solidus Kerns, kein Neubau. Eine sinnvolle Reihenfolge folgt dem Risiko:
- Die Fahrzeugdaten und die Regel mit genau einer Einheit, weil alles andere daraus liest.
- Der Mietkalender mit seinem Exclusion Constraint, weil eine Doppelbuchung der teuerste Fehler ist.
- Die Mietpreise in einem Price Selector, damit jede Neuberechnung übereinstimmt.
- Die Kaution als Autorisierung, mit dem Zeitfenster der Reservierung im Kopf.
- Die Checkoutschritte und die Dokumente.
Mails und Tracking sind eigene Themen: Wie du Bestellmails sicher verschickst, steht in Postmark mit Rails und Solidus, und Tracking mit Einwilligung in Event Tracking und Consent Mode. Wer das Ganze in einem echten Betrieb sehen will: Die Fallstudie zur Autohaus Plattform zeigt Verkauf, Vermietung, Werkstatt, Gutscheine und Marketing aus einem Admin.
Footnotes
-
Solidus, Solidus README, GitHub, 2026, https://github.com/solidusio/solidus
-
Solidus, core/app/models/spree/product.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/product.rb
-
Solidus, core/app/models/spree/stock_item.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/stock_item.rb
-
Solidus, core/app/models/spree/variant.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/variant.rb
-
Solidus, core/app/models/spree/option_type.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/option_type.rb
-
Solidus, Customizing the core, Solidus Guides, 2026, https://guides.solidus.io/customization/customizing-the-core/
-
Solidus, core/app/models/spree/stock_location.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/stock_location.rb
-
Solidus, Stock management, Solidus Guides, 2026, https://guides.solidus.io/advanced-solidus/stock-and-fulfillment/
-
The PostgreSQL Global Development Group, Range Types (Abschnitt 8.17), PostgreSQL Documentation, 2026, https://www.postgresql.org/docs/current/rangetypes.html
-
Ruby on Rails, Active Record and PostgreSQL, Rails Guides, 2026, https://guides.rubyonrails.org/active_record_postgresql.html
-
Solidus, core/app/models/spree/variant/price_selector.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/variant/price_selector.rb
-
Solidus, core/app/models/spree/variant/pricing_options.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/variant/pricing_options.rb
-
Solidus, core/lib/spree/app_configuration.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/lib/spree/app_configuration.rb
-
Solidus, core/lib/spree/permitted_attributes.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/lib/spree/permitted_attributes.rb
-
Solidus, Payments and refunds, Solidus Guides, 2026, https://guides.solidus.io/advanced-solidus/payments-and-refunds/
-
Solidus, core/app/models/spree/payment/processing.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/payment/processing.rb
-
Stripe, Place a hold on a payment method, Stripe Documentation, 2026, https://docs.stripe.com/payments/place-a-hold-on-a-payment-method
-
Solidus, Redefining checkout steps, Solidus Guides, 2026, https://guides.solidus.io/cookbook/redefining-checkout-steps/
-
Solidus, core/app/models/spree/core/state_machines/order/class_methods.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/core/state_machines/order/class_methods.rb
-
Ruby on Rails, Active Storage Overview, Rails Guides, 2026, https://guides.rubyonrails.org/active_storage_overview.html
-
Bundesministerium der Justiz, Straßenverkehrsgesetz (StVG), § 21 Fahren ohne Fahrerlaubnis, gesetze im internet, 2026, https://www.gesetze-im-internet.de/stvg/__21.html
-
Bundesministerium der Justiz, Bürgerliches Gesetzbuch (BGB), § 312g Widerrufsrecht, Absatz 1 und Absatz 2 Satz 1 Nr. 9, gesetze im internet, 2026, https://www.gesetze-im-internet.de/bgb/__312g.html
-
Bundesministerium der Justiz, Bürgerliches Gesetzbuch (BGB), § 355 Widerrufsrecht bei Verbraucherverträgen, gesetze im internet, 2026, https://www.gesetze-im-internet.de/bgb/__355.html
-
Bundesministerium der Justiz, Bürgerliches Gesetzbuch (BGB), § 356 Widerrufsrecht bei außerhalb von Geschäftsräumen geschlossenen Verträgen und Fernabsatzverträgen, gesetze im internet, 2026, https://www.gesetze-im-internet.de/bgb/__356.html
-
Bundesministerium der Justiz, Umsatzsteuergesetz (UStG), § 25a Differenzbesteuerung, gesetze im internet, 2026, https://www.gesetze-im-internet.de/ustg_1980/__25a.html
-
Bundesministerium der Justiz, Umsatzsteuergesetz (UStG), § 12 Steuersätze, gesetze im internet, 2026, https://www.gesetze-im-internet.de/ustg_1980/__12.html
-
Solidus, Taxation, Solidus Guides, 2026, https://guides.solidus.io/advanced-solidus/tax-calculation/