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.
  • Österreichische Regeln, auf die du baust: 14 Tage Rücktritt bei Autos aus dem Fernabsatz, kein Rücktritt bei Mieten mit festem Termin, die Pflicht des Zulassungsbesitzers bei der Lenkberechtigung und die Differenzbesteuerung mit 20 %.

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 Linz und Graz, 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 Häferl 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, der Zulassungsschein.

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 österreichischen 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 Linz

Das Autohaus Donaublick ist erfunden. Theresa führt es mit zwei Filialen, eine in Linz und eine in Graz. Sie verkauft Gebrauchtwagen und vermietet eine kleine Flotte Kompaktwagen, und ihr Shop liegt unter autohausdonaublick.example.

Drei Aufgaben müssen auf einem System laufen:

  1. Gebrauchtwagen verkaufen, jeder ein Einzelstück, zu Preisen zwischen etwa € 8.000 und € 25.000.
  2. Flottenautos tageweise vermieten, abgeholt und zurückgegeben in einer Filiale.
  3. 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 Linz

Theresa kauft einen Volkswagen Golf von 2021 und stellt ihn in Linz auf den Hof.

  1. Sie legt ein Produkt an, "2021 Volkswagen Golf Life 1.5 TSI, blau", zum Preis von € 16.495.
  2. Solidus legt dazu die Mastervariante an. Sie bekommt die SKU DB-0412.
  3. Am Lagerort Linz bekommt das Stock Item eine Menge von 1, nachbestellbar aus. In Graz ist die Menge 0.
  4. Eine Käuferin zahlt die Anzahlung online. Der Checkout schließt ab, und die Menge in Linz geht von 1 auf 0.
  5. 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 ist kein eigener Code nötig gewesen. Das Bestandsmodell versteht "eins von eins" eh 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

Theresa richtet ihr Mietangebot so ein (das Beispiel rechnet in Kilometern):

  1. Ein Produkt, "Kompaktwagen mit Automatik", im Taxon "Miete, dann Klein".
  2. Ein Optionstyp, "Kilometer", mit zwei Werten: "161 km am Tag" und "Unbegrenzt".
  3. 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.
  4. 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" passt das. 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 Donaublick

Die Migration legt eine Zeile pro Auto an, verknüpft mit seinem Produkt:

create_table :donaublick_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 Donaublick
  module Spree
    module Product
      module AddVehicle
        def self.prepended(base)
          base.has_one :vehicle, class_name: "Donaublick::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(donaublick_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 Donaublick 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 zu seinem Auto. Abholung in der Filiale oder Zustellung durch einen Fahrer.

Beispiel: ein Käufer in Graz will den Golf aus Linz

Bevor der Verkauf durch ist, fragt ein Käufer in Graz nach dem blauen Golf.

  1. Das Stock Item des Golfs in Linz hält 1. In Graz hält es 0.
  2. Theresa 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.
  3. Der Käufer holt lieber in Graz ab. Theresas Team überstellt das Auto.
  4. Im Admin senken sie die Menge in Linz um eins und erhöhen die in Graz um eins. Linz zeigt jetzt 0, Graz zeigt 1.
  5. Der Käufer schließt den Checkout ab. Der Coordinator findet die Einheit in Graz, 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.

Wie eine Mietbuchung ein Auto belegtEin Kunde wählt Daten und eine Mietklasse. Das System probiert jedes Flottenauto dieser Klasse. Nimmt die Datenbank die Buchung ohne Überlappung an, sind die Daten reserviert und der Kunde geht zum Checkout. Sind alle Autos belegt, erfährt der Kunde, dass die Daten nicht verfügbar sind.Kunde wählt Mietklasse und DatenDas nächste Flottenauto dieser KlassenehmenEine Buchung für dieses Auto und dieseDaten einfügenLehnt die Datenbank eineÜberlappung ab?jaJa: gibt es noch ein Auto in derKlasse?neinNein: Daten reservieren und zumCheckoutIst kein Auto mehr übrig, die Daten alsnicht verfügbar zeigen

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 Linz. Die Buchungstabelle hat einen Verweis auf das Auto und ein daterange namens period:

enable_extension "btree_gist"

create_table :donaublick_bookings do |t|
  t.references :donaublick_fleet_car, null: false, foreign_key: true
  t.daterange :period, null: false
  t.exclusion_constraint "donaublick_fleet_car_id WITH =, period WITH &&", using: :gist # keine zwei Buchungen desselben Autos mit überlappendem Zeitraum
  t.timestamps
end

Drei Kunden wollen es buchen.

  1. Buchung A, Abholung Freitag, 14. August 2026, Rückgabe Montag, 17. August. Gespeichert als [2026-08-14, 2026-08-17). Angenommen.
  2. 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.
  3. 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:

P=(1−d(n))∑t=1nb wt stP = \left(1 - d(n)\right) \sum_{t=1}^{n} b \, w_t \, s_t

Dabei ist PP der Mietpreis, nn die Zahl der Miettage, tt zählt die Tage von 11 bis nn, bb ist der Grundpreis der Variante pro Tag, wtw_t der Wochenendfaktor für Tag tt (zum Beispiel 1,251{,}25 an einem Samstag oder Sonntag, sonst 11), sts_t der Saisonfaktor für Tag tt (zum Beispiel 1,201{,}20 im August, sonst 11) und d(n)d(n) der Rabatt für die Mietdauer als Anteil, zum Beispiel 0,100{,}10, wenn nn sieben oder mehr ist, und darunter 00. 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 händisch 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 n=7n = 7 Tage: Freitag, Samstag, Sonntag, Montag, Dienstag, Mittwoch, Donnerstag. Der Grundpreis ist b=42b = 42 Euro. Alle sieben Tage liegen im August, also gilt jeden Tag st=1,20s_t = 1{,}20. Samstag und Sonntag haben wt=1,25w_t = 1{,}25. Diese Faktoren sind Annahmen dieses Beispiels, gewählt, um die Rechnung zu zeigen.

  1. Der Preis an einem Wochentag: 42×1×1,20=50,4042 \times 1 \times 1{,}20 = 50{,}40.
  2. Der Preis an einem Wochenendtag: 42×1,25×1,20=63,0042 \times 1{,}25 \times 1{,}20 = 63{,}00.
  3. Die sieben Tage addieren, fünf Wochentage und zwei Wochenendtage: 5×50,40+2×63,00=252,00+126,00=378,005 \times 50{,}40 + 2 \times 63{,}00 = 252{,}00 + 126{,}00 = 378{,}00.
  4. Der Rabatt für die Dauer. Sieben Tage ergeben d(7)=0,10d(7) = 0{,}10, also P=(1−0,10)×378,00=340,20P = (1 - 0{,}10) \times 378{,}00 = 340{,}20.

Der Kunde zahlt € 340,20. In diesem Beispiel sind die Preise inklusive Umsatzsteuer. Zum Vergleich: Das Wochenende von Freitag bis Montag kostet 50,40+63,00+63,00=176,4050{,}40 + 63{,}00 + 63{,}00 = 176{,}40, ohne Rabatt, weil drei unter sieben liegt.

Tagespreis der Miete über die WocheSieben Balken, einer pro Tag von Freitag, 14. August, bis Donnerstag, 20. August 2026. Samstag und Sonntag liegen bei € 63,00, jeder andere Tag bei € 50,40.Kleiner Automatik, Tagespreis vor dem Rabatt für die Dauer€ 0,00€ 25,00€ 50,00€ 75,00€ 100,00€ 50,40Fr 14.€ 63,00Sa 15.€ 63,00So 16.€ 50,40Mo 17.€ 50,40Di 18.€ 50,40Mi 19.€ 50,40Do 20.Tagespreis in €Miettag

Die waagrechte 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. Theresas Kaution beträgt € 250, eine Annahme dieses Beispiels.

  1. Bei der Buchung zahlt die Kundin den Mietpreis von € 176,40. Er wird sofort eingezogen.
  2. Am Freitag in der Früh 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.
  3. Am Montag kommt das Auto mit einem Viertel Tank zu wenig zurück. Theresas Gebühr fürs Nachtanken beträgt € 38, wieder eine Annahme.
  4. Ihr Team zieht € 38 der € 250 ein. In Cent ist das capture!(3800).
  5. Stripe gibt die übrigen € 212 an die Kundin frei: € 250 minus € 38.
  6. 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 Donaublick 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 Donaublick
  module Spree
    module Order
      module AddLicenceStep
        def self.prepended(base)
          base.insert_checkout_step :licence, before: :payment, if: ->(order) { order.rental? } # der Schritt erscheint nur, wenn die Bestellung eine Miete enthält
        end

        ::Spree::Order.prepend self
      end
    end
  end
end

Die Methode rental? gehört dem Autohaus Donaublick 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

Den Führerschein prüft die Vermietung selbst, im Original bei der Abholung. Warum das zählt: Nach dem Kraftfahrgesetz darf der Zulassungsbesitzer das Lenken seines Kraftfahrzeugs nur Personen überlassen, die die erforderliche Lenkberechtigung und das erforderliche Mindestalter besitzen und für die kein Lenkverbot gilt.21 Bei einem Mietwagen ist der Zulassungsbesitzer 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.

Ein österreichisches Papier gehört an den Fahrzeugdatensatz, nicht an die Buchung: das Gutachten der wiederkehrenden Begutachtung, im Alltag einfach das Pickerl. Die ermächtigte Prüfstelle stellt das Gutachten auf einem Formblatt aus, es ist eine öffentliche Urkunde, und die Begutachtungsplakette am Auto zeigt, bis wann die nächste Begutachtung fällig ist.22 Ein Scan des letzten Gutachtens hängt mit has_one_attached :inspection_report am Fahrzeug, und der Käufer sieht ihn vor der Anzahlung.

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 Lenkverbot dazukommen. Die Pflicht des Zulassungsbesitzers gilt für den Moment, in dem er jemandem das Lenken überlässt.

Also hält der Führerscheinschritt im Checkout nur die Führerscheinnummer, die Klasse und die Einwilligung fest. Dann:

  1. Sieben Tage vor der Abholung, am 7. August, erinnert eine Mail daran, den Führerschein im Original mitzubringen.
  2. 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.
  3. Sie markiert die Buchung mit Datum als "Führerschein geprüft", und das Ergebnis wird an der Buchung gespeichert.
  4. Der unterschriebene Mietvertrag wird als PDF erzeugt und mit has_one_attached :contract an die Buchung gehängt, zusammen mit den Fotos bei der Übergabe.

Kommt im Oktober ein Streit auf, liegt alles an einem Datensatz.

Die österreichischen Regeln, die den Bau prägen

Ein paar österreichische Regeln ändern, wie sich die Software verhalten muss. Kurz gefasst, und keine Rechtsberatung:

Rücktritt bei online verkauften Autos. Bei einem Fernabsatzvertrag kann ein Verbraucher nach dem Fern und Auswärtsgeschäftegesetz (FAGG) binnen 14 Tagen ohne Angabe von Gründen zurücktreten. Bei einem Kaufvertrag über eine Ware beginnt die Frist mit dem Tag, an dem der Verbraucher den Besitz an der Ware erlangt.23 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 Dienstleistungen im Bereich Vermietung von Kraftfahrzeugen vom Rücktrittsrecht aus, sofern für die Vertragserfüllung ein bestimmter Zeitpunkt oder Zeitraum vertraglich vorgesehen ist, so steht es in § 18 Abs. 1 Z 10.24 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 der Händler gewerbsmäßig mit solchen Gegenständen handelt und dass für die Lieferung an ihn keine Umsatzsteuer geschuldet war oder sie selbst schon differenzbesteuert war.25 Bemessen wird der Betrag, um den der Verkaufspreis den Einkaufspreis übersteigt, die Umsatzsteuer gehört nicht zur Bemessungsgrundlage, und es gilt der Normalsteuersatz von 20 %.2526 Marge und Steuer ergeben sich in zwei Schritten:

m=psell−pbuy,USt=m⋅20120=m6m = p_{\text{sell}} - p_{\text{buy}}, \qquad \text{USt} = m \cdot \frac{20}{120} = \frac{m}{6}

Dabei ist mm die Bruttomarge, psellp_{\text{sell}} der Verkaufspreis, pbuyp_{\text{buy}} der Einkaufspreis und 20120\frac{20}{120} der Anteil der Steuer an einem Betrag, der 20 % Umsatzsteuer schon enthält, gekürzt 16\frac{1}{6}. Er folgt daraus, dass die Steuer nicht zur Bemessungsgrundlage gehört: Die Bemessungsgrundlage ist m1,20\frac{m}{1{,}20}, und 2020 % davon sind 0,20 m1,20=m⋅20120=m6\frac{0{,}20 \, m}{1{,}20} = m \cdot \frac{20}{120} = \frac{m}{6}, siehe Grundlagen: Prozente.

Beispiel: die Umsatzsteuer auf den blauen Golf

Theresa hat den Golf für € 14.200 von einem Privatverkäufer gekauft. Für diese Lieferung ist keine Umsatzsteuer geschuldet gewesen, also passt das Auto in die Differenzbesteuerung. Sie verkauft es für € 16.495.

  1. Die Marge: m=16.495−14.200=2.295m = 16.495 - 14.200 = 2.295.
  2. Die Bemessungsgrundlage: 2.295÷1,20=1.912,502.295 \div 1{,}20 = 1.912{,}50.
  3. Die Steuer: 2.295×20120=2.2956=382,502.295 \times \frac{20}{120} = \frac{2.295}{6} = 382{,}50.
  4. Zur Probe: 1.912,50+382,50=2.295,001.912{,}50 + 382{,}50 = 2.295{,}00.

Sie schuldet auf diesen Verkauf € 382,50 Umsatzsteuer, nicht die € 2.749,17 (16.495÷616.495 \div 6), die auf den vollen Preis fällig wären. Wer die deutsche Ausgabe dieses Artikels kennt: Dort stehen 366,43 Euro, weil der deutsche Regelsatz 19 % beträgt und der Bruch dort 19119\frac{19}{119} heißt. Die britische Ausgabe landet dagegen beim selben Bruch wie hier, 16\frac{1}{6}, weil der britische Normalsatz ebenfalls 20 % beträgt, nur in Pfund. 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:

  1. Die Fahrzeugdaten und die Regel mit genau einer Einheit, weil alles andere daraus liest.
  2. Der Mietkalender mit seinem Exclusion Constraint, weil eine Doppelbuchung der teuerste Fehler ist.
  3. Die Mietpreise in einem Price Selector, damit jede Neuberechnung übereinstimmt.
  4. Die Kaution als Autorisierung, mit dem Zeitfenster der Reservierung im Kopf.
  5. 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

  1. Solidus, Solidus README, GitHub, 2026, https://github.com/solidusio/solidus

  2. Solidus, core/app/models/spree/product.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/product.rb

  3. Solidus, core/app/models/spree/stock_item.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/stock_item.rb

  4. Solidus, core/app/models/spree/variant.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/variant.rb

  5. Solidus, core/app/models/spree/option_type.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/option_type.rb

  6. Solidus, Customizing the core, Solidus Guides, 2026, https://guides.solidus.io/customization/customizing-the-core/

  7. Solidus, core/app/models/spree/stock_location.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/stock_location.rb

  8. Solidus, Stock management, Solidus Guides, 2026, https://guides.solidus.io/advanced-solidus/stock-and-fulfillment/

  9. The PostgreSQL Global Development Group, Range Types (Abschnitt 8.17), PostgreSQL Documentation, 2026, https://www.postgresql.org/docs/current/rangetypes.html

  10. Ruby on Rails, Active Record and PostgreSQL, Rails Guides, 2026, https://guides.rubyonrails.org/active_record_postgresql.html

  11. 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

  12. 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

  13. Solidus, core/lib/spree/app_configuration.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/lib/spree/app_configuration.rb

  14. Solidus, core/lib/spree/permitted_attributes.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/lib/spree/permitted_attributes.rb

  15. Solidus, Payments and refunds, Solidus Guides, 2026, https://guides.solidus.io/advanced-solidus/payments-and-refunds/

  16. Solidus, core/app/models/spree/payment/processing.rb, GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/models/spree/payment/processing.rb

  17. Stripe, Place a hold on a payment method, Stripe Documentation, 2026, https://docs.stripe.com/payments/place-a-hold-on-a-payment-method

  18. Solidus, Redefining checkout steps, Solidus Guides, 2026, https://guides.solidus.io/cookbook/redefining-checkout-steps/

  19. 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

  20. Ruby on Rails, Active Storage Overview, Rails Guides, 2026, https://guides.rubyonrails.org/active_storage_overview.html

  21. Bundeskanzleramt, Kraftfahrgesetz 1967 (KFG 1967), § 103 Pflichten des Zulassungsbesitzers, Abs. 1 Z 3 lit. a, Rechtsinformationssystem des Bundes (RIS), 2026, https://www.ris.bka.gv.at/Dokumente/Bundesnormen/NOR40253991/NOR40253991.html

  22. Bundeskanzleramt, Kraftfahrgesetz 1967 (KFG 1967), § 57a Wiederkehrende Begutachtung, Abs. 4 und 5, Rechtsinformationssystem des Bundes (RIS), 2026, https://www.ris.bka.gv.at/Dokumente/Bundesnormen/NOR40279590/NOR40279590.html

  23. Bundeskanzleramt, Fern und Auswärtsgeschäftegesetz (FAGG), § 11 Rücktrittsrecht und Rücktrittsfrist, Abs. 1 und Abs. 2 Z 2 lit. a, Rechtsinformationssystem des Bundes (RIS), 2026, https://www.ris.bka.gv.at/Dokumente/Bundesnormen/NOR40162337/NOR40162337.html

  24. Bundeskanzleramt, Fern und Auswärtsgeschäftegesetz (FAGG), § 18 Ausnahmen vom Rücktrittsrecht, Abs. 1 Z 10, Rechtsinformationssystem des Bundes (RIS), 2026, https://www.ris.bka.gv.at/Dokumente/Bundesnormen/NOR40245435/NOR40245435.html

  25. Bundeskanzleramt, Umsatzsteuergesetz 1994 (UStG 1994), § 24 Differenzbesteuerung, Abs. 1, 4 und 6, Rechtsinformationssystem des Bundes (RIS), 2026, https://www.ris.bka.gv.at/Dokumente/Bundesnormen/NOR40263531/NOR40263531.html

  26. Bundeskanzleramt, Umsatzsteuergesetz 1994 (UStG 1994), § 10 Steuersätze, Abs. 1, Rechtsinformationssystem des Bundes (RIS), 2026, https://www.ris.bka.gv.at/Dokumente/Bundesnormen/NOR40278068/NOR40278068.html

  27. Solidus, Taxation, Solidus Guides, 2026, https://guides.solidus.io/advanced-solidus/tax-calculation/

Wenn du ein Autohaus oder eine Vermietung führst und Bestand, Buchungen und Kautionen in drei verschiedenen Systemen liegen, bring sie in einen kostenlosen Workshop mit, und wir legen sie zusammen auf ein System um. Die ersten zwei Wochen Arbeit gehen auf meine Rechnung.