Όλα τα έργαEN

Μελέτη περίπτωσης · International Rentals

Σε εξέλιξη

Διαχείριση στόλου, χωρίς χαρτί

Ελληνική εταιρεία rent-a-car κινεί περίπου 100 αυτοκίνητα μέσα από αντιπροσώπους σε ξενοδοχεία. Η διαθεσιμότητα, οι τιμές, τα συμφωνητικά και τα μετρητά ζουν σε χαρτί και σε τηλεφωνήματα. Αυτή είναι η πλατφόρμα που τα αντικαθιστά, από άκρη σε άκρη.

Γιατί δεν υπάρχει σύνδεσμος

Δεν υπάρχει κάτι να ανοίξεις. Πρόκειται για εσωτερικό εργαλείο πίσω από σύνδεση, που κρατά τον στόλο, τους πελάτες και τα χρήματα μιας πραγματικής εταιρείας. Οπότε αυτή η σελίδα δείχνει το ίδιο το έργο: τις οθόνες, τους κανόνες από κάτω και τη σκέψη που οδήγησε εκεί.

  • ~100

    αυτοκίνητα, δεσμευμένα ανά ημέρα και όχι ανά ώρα

  • 6-10

    αντιπρόσωποι σε ξενοδοχεία, συν έναν ιδιοκτήτη

  • 2

    γλώσσες, 1.082 κείμενα η καθεμία, με τεστ που τις κρατά ίσες

  • 47

    αρχεία τεστ, τα 31 πάνω σε πραγματική Postgres

Το πρόβλημα

Όλα δούλευαν, και τίποτα δεν ήταν γραμμένο δύο φορές

Η εταιρεία δουλεύει όπως δουλεύουν πολλές νησιωτικές επιχειρήσεις ενοικιάσεων. Ο επισκέπτης ρωτά στη ρεσεψιόν, ο αντιπρόσωπος τηλεφωνεί στο γραφείο, κάποιος κοιτά ένα χαρτί, και το αυτοκίνητο είτε είναι ελεύθερο είτε όχι. Στέκει επειδή οι ίδιοι άνθρωποι το κάνουν χρόνια.

Αυτό που δεν μπορεί να κάνει είναι να απαντήσει την ίδια ερώτηση δύο φορές με τον ίδιο τρόπο. Δύο αντιπρόσωποι μπορούν να υποσχεθούν το ίδιο αυτοκίνητο μέσα στο ίδιο λεπτό. Μια τιμή που δόθηκε σε ένα ξενοδοχείο μπορεί να διαφέρει από την τιμή στο επόμενο. Ένας επισκέπτης που επιστρέφει νωρίς ελευθερώνει ένα αυτοκίνητο που κανείς δεν ξέρει ότι είναι ελεύθερο. Και καθένα από αυτά κοστίζει ακριβώς τη στιγμή που η σεζόν είναι στο φόρτε της.

Οπότε το ζητούμενο δεν ήταν στην ουσία λογισμικό. Ήταν: αντικατέστησε το χαρτί χωρίς να χαλάσεις τη μέρα, και μη γίνεις ποτέ ο λόγος που ένα αυτοκίνητο υπόσχεται σε δύο ανθρώπους.

Δύο χρήστες

Ένας κώδικας για δύο ανθρώπους που δεν πρέπει να βλέπουν τα ίδια

Ο αντιπρόσωπος δουλεύει με το ένα χέρι, σε κινητό, σε μια ρεσεψιόν, στον ήλιο, με τον πελάτη μπροστά του. Ο ιδιοκτήτης δουλεύει σε desktop και χρειάζεται όλη την εταιρεία μαζί. Ίδια εφαρμογή, ίδιο deploy, δύο εντελώς διαφορετικές δουλειές.

Ο εμπορικός κανόνας είναι ωμός: ο αντιπρόσωπος δεν βλέπει ποτέ συγκεντρωτικό στοιχείο. Ούτε πλήθος, ούτε άθροισμα, ούτε μέσο όρο. Αν ένας αριθμός στην οθόνη του επιτρέπει να συναχθεί ο τζίρος της εταιρείας, δεν μπαίνει στην οθόνη. Η μόνη εξαίρεση είναι τα δικά του μετρητά της ημέρας, γιατί αυτός είναι που τα κουβαλά.

Ο κανόνας δεν επιβάλλεται κρύβοντας κουμπιά. Η εξουσιοδότηση ζει στη βάση: row-level security ενεργό σε κάθε πίνακα, με πολιτικές που δοκιμάζονται από τη συνεδρία του ίδιου του αντιπροσώπου. Όποιος πειράξει ένα API call με το χέρι βρίσκει τον ίδιο ακριβώς τοίχο, γιατί ο τοίχος δεν είναι στον route handler.

Η εφαρμογή του αντιπροσώπου στο κινητό δίπλα στο φύλλο ημέρας του ιδιοκτήτη
Η μέρα του αντιπροσώπου και το φύλλο ημέρας του ιδιοκτήτη, από ένα deploy. Κάθε πινακίδα, επισκέπτης και ξενοδοχείο εδώ είναι επινοημένα.Άνοιγμα σε πλήρες μέγεθος

Ορθότητα

Δύο αντιπρόσωποι κυνηγούν το τελευταίο αυτοκίνητο, και η βάση αρνείται

Η διπλή κράτηση είναι η αποτυχία που μετράει, οπότε δεν αφήνεται σε κώδικα εφαρμογής που μια μελλοντική αλλαγή μπορεί να αναιρέσει. Οι ενοικιάσεις και τα διοικητικά μπλοκαρίσματα ζουν σε έναν πίνακα, και ένα exclusion constraint της Postgres καλύπτει και τα δύο. Δύο επικαλυπτόμενες δεσμεύσεις στο ίδιο αυτοκίνητο δεν μπορούν να υπάρξουν, γιατί η ίδια η εγγραφή απορρίπτεται.

Μια αποτυχημένη εγγραφή σημαίνει λοιπόν κάτι αληθινό: το αυτοκίνητο είναι όντως πιασμένο. Ο αντιπρόσωπος το μαθαίνει με απλά λόγια, αντί να του δοθεί ένα αυτοκίνητο που έχει φύγει.

Το άλλο μισό της ορθότητας είναι ο κανόνας της ημέρας. Η μέρα εδώ είναι από το πρωί ως το βράδυ, οπότε παραλαβή Δευτέρα με επιστροφή Τετάρτη είναι τρεις ημέρες και το αυτοκίνητο κρατιέται όλη την Τετάρτη. Κάθε υπολογισμός ημερομηνίας στον κώδικα περιλαμβάνει και τα δύο άκρα. Δύο κρατήσεις μπορούν να ακουμπούν χωρίς να επικαλύπτονται: μία που λήγει στις 15 και μία που αρχίζει στις 16 είναι νόμιμες.

Οι αντιπρόσωποι δεν ρωτούν ποτέ τον πίνακα κρατήσεων για διαθεσιμότητα. Καλούν μία συνάρτηση που επιστρέφει ids αυτοκινήτων και πιασμένες ημερομηνίες και τίποτε άλλο, ώστε το να μάθεις ότι ένα αυτοκίνητο είναι πιασμένο να μην αποκαλύπτει και ποιος το πήρε, από ποιο ξενοδοχείο, με πόσα ή γιατί.

Η εγγύηση, σε ένα constraint
EXCLUDE USING gist (
  car_id WITH =,
  daterange(start_date, end_date, '[]') WITH &&
) WHERE (status IN ('booked','out','blocked'))
Το exclusion constraint, το σφάλμα της Postgres και το μήνυμα που βλέπει ο αντιπρόσωπος
Ένα σφάλμα βάσης που κανείς δεν πρέπει να διαβάσει, μεταφρασμένο σε μία πρόταση που ο αντιπρόσωπος μπορεί να χρησιμοποιήσει.Άνοιγμα σε πλήρες μέγεθος

Τιμολόγηση

Ο τιμοκατάλογος δεν φτάνει ποτέ στο κινητό του αντιπροσώπου

Η τιμολόγηση τρέχει στον server και μόνο εκεί. Η συσκευή του αντιπροσώπου ζητά προσφορά και εμφανίζει τον αριθμό που παίρνει πίσω. Δεν μπορεί να τον ξαναϋπολογίσει, και δεν κρατά ποτέ τον πίνακα από τον οποίο προέκυψε.

Οι κανόνες είναι του ίδιου του πελάτη, οπότε η εφαρμογή τους ακολουθεί αντί να τους βελτιώνει. Τα σύνολα καταχωρούνται ανά περίοδο, κατηγορία και αριθμό ημερών, και περιέχουν ήδη την προσαύξηση της πρώτης ημέρας, οπότε η εφαρμογή δεν προσθέτει τίποτα από πάνω. Πέρα από τις επτά ημέρες ισχύει χρέωση ανά επιπλέον ημέρα. Τα παιδικά καθίσματα και οι πρόσθετοι οδηγοί είναι δωρεάν και παραμένουν δωρεάν.

Δύο λεπτομέρειες κρατούν τα χρήματα τίμια στον χρόνο. Τα ποσά είναι ακέραια λεπτά παντού, από τη βάση ως το PDF, ώστε κανένα δεκαδικό να μην αγγίζει σύνολο. Και η περίοδος τιμολόγησης αποθηκεύεται πάνω στην ίδια την κράτηση, ώστε η επεξεργασία του τιμοκαταλόγου της επόμενης σεζόν να μην ξαναγράφει σιωπηλά αυτό που ένας επισκέπτης έχει ήδη συμφωνήσει.

Αν η ημερομηνία παραλαβής δεν πέφτει σε καμία ορισμένη περίοδο, η προσφορά αποτυγχάνει θορυβωδώς. Δεν μαντεύει, και δεν δανείζεται τους αριθμούς της διπλανής περιόδου.

Μια νέα κράτηση στο κινητό του αντιπροσώπου δίπλα στον κανόνα τιμολόγησης του server
Ο αντιπρόσωπος βλέπει έναν αριθμό για την κράτηση που έχει μπροστά του, με τις ημέρες από τις οποίες προέκυψε.Άνοιγμα σε πλήρες μέγεθος

Το χαρτί

Ό,τι έκανε το ντοσιέ, με τη σειρά που συμβαίνει

Η παραλαβή είναι μια ακολουθία, ένα πράγμα ανά οθόνη, και επιβιώνει αν η εφαρμογή κλείσει στη μέση. Φωτογραφίες διπλώματος, μετά ο έλεγχος καταλληλότητας, μετά τα καύσιμα, μετά οι υπάρχουσες ζημιές πάνω σε διάγραμμα του οχήματος, μετά το συμφωνητικό και η υπογραφή, και τέλος η πληρωμή.

Τα διπλώματα διαβάζονται στον server με vision OCR, και τα πεδία γυρίζουν επεξεργάσιμα. Το κείμενο που εξάγεται αντιμετωπίζεται ως μη έμπιστη είσοδος από την κάρτα ενός αγνώστου: περνά σε αυστηρό σχήμα, ό,τι δεν ανήκει εκεί απορρίπτεται, και υπάρχει όριο κλήσεων ανά χρήστη. Μια αποτυχημένη σάρωση δεν κοστίζει τίποτα, η πληκτρολόγηση δουλεύει πάντα.

Ο έλεγχος καταλληλότητας είναι απόλυτο στοπ. Ηλικία σε σχέση με το ελάχιστο της κατηγορίας, δίπλωμα τουλάχιστον ενός έτους, λήξη μετά την ημερομηνία επιστροφής. Η οθόνη ονομάζει τον κανόνα που απέτυχε και δίνει στον αντιπρόσωπο μόνο ένα αίτημα έγκρισης προς τον διαχειριστή, και η έγκριση καταγράφεται όταν δοθεί.

Το συμφωνητικό παράγεται ως δίγλωσσο PDF με τους δύο οδηγούς, τη στάθμη καυσίμου, το διάγραμμα ζημιών και την υπογραφή του επισκέπτη στην οθόνη. Χτίζεται ντετερμινιστικά στον server, χωρίς headless browser να συντηρείται στην παραγωγή.

Η ακολουθία παραλαβής, ένας αποτυχημένος έλεγχος καταλληλότητας και το δίγλωσσο συμφωνητικό
Ένας επισκέπτης πολύ νέος για την κατηγορία δεν μπορεί να πάρει το αυτοκίνητο, και ο αντιπρόσωπος δεν καλείται να το κρίνει.Άνοιγμα σε πλήρες μέγεθος

Δύο γλώσσες

Ελληνικά και αγγλικά από το πρώτο commit

Οι αντιπρόσωποι είναι Έλληνες. Οι μισοί επισκέπτες δεν είναι. Το να προστεθεί δεύτερη γλώσσα σε μια εφαρμογή που ήδη δουλεύει είναι ξαναγράψιμο, οπότε σε αυτόν τον κώδικα δεν υπήρξε ποτέ κείμενο χρήστη γραμμένο μέσα στον κώδικα.

Και οι δύο γλώσσες κρατούν 1.082 κείμενα, και ένα τεστ ρίχνει το build αν η μία αποκτήσει κλειδί που δεν έχει η άλλη. Η γλώσσα είναι ρύθμιση ανά χρήστη και όχι εικασία του browser, γιατί το κινητό του αντιπροσώπου είναι δικό του, ενώ οι προσδοκίες του επισκέπτη δεν είναι.

Το συμφωνητικό είναι δίγλωσσο στην ίδια σελίδα και όχι σε δύο έγγραφα, που είναι αυτό που πραγματικά χρειάζεται ο επισκέπτης που υπογράφει σε μια ρεσεψιόν, και το ίδιο χρειάζεται και η ελληνική εταιρεία που το εκδίδει.

Ο χώρος όπου χρησιμοποιείται

Ένα κινητό, ένα χέρι, ήλιος, και ένας επισκέπτης που περιμένει

Είναι εργαλείο λειτουργίας που χρησιμοποιείται είκοσι φορές τη μέρα, όχι βιτρίνα. Η κίνηση υπάρχει για να εξηγήσει μια αλλαγή κατάστασης και μετά να φύγει από τη μέση. Η αναγνωσιμότητα κερδίζει την κομψότητα, και οι μεγάλοι στόχοι κερδίζουν την πυκνότητα.

Το WCAG 2.1 AA είναι το δάπεδο, όχι η φιλοδοξία: κάθε ζεύγος χρώματος και φόντου στα design tokens το περνά, και ένα unit test υπολογίζει τους λόγους από τις ίδιες τις τιμές αντί να εμπιστεύεται ένα σχόλιο που μπορεί να έχει παλιώσει. Τα βήματα της κάμερας και της υπογραφής έχουν μη οπτικές διαδρομές, γιατί μια ροή που δουλεύει μόνο με την όραση είναι μια ροή που αποκλείει ανθρώπους.

Διατίθεται στο Android ως Trusted Web Activity αντί για δεύτερο native κώδικα, κάτι που κρατά την καταχώριση στο Play, την κάμερα και τις ειδοποιήσεις, ενώ επιτρέπει μια διόρθωση μέσα στη σεζόν να βγει σε λεπτά αντί να περιμένει σε ουρά ελέγχου.

Τι το κρατά όρθιο

Τα τεστ είναι αυτό που επιτρέπει να αλλάξει

47 αρχεία τεστ, τα 31 από τα οποία τρέχουν πάνω σε πραγματική Postgres με τις ίδιες πολιτικές που βγαίνουν στην παραγωγή. Χωρίς παρακάμψεις service-role και χωρίς route handlers στη μέση: μια συνεδρία αντιπροσώπου που κάνει ό,τι χειρότερο μπορεί, ελεγμένη απέναντι σε αυτό που της επιτρέπεται να δει.

Οι δύο μηχανές γράφτηκαν πριν από τις οθόνες που τις χρησιμοποιούν, γιατί μια λάθος απάντηση διαθεσιμότητας ή μια λάθος τιμή δεν είναι οπτικό σφάλμα και δεν θα φαινόταν με το μάτι.

45 migrations, κάθε εγγραφή καταγεγραμμένη, και ένα μόνιμο αρχείο ελέγχου μόνο για ανάγνωση που ο ιδιοκτήτης φιλτράρει ανά χρήστη, οντότητα και ημερομηνία. Όταν κάτι αμφισβητηθεί τον Αύγουστο, η απάντηση υπάρχει.

Πού βρίσκεται

  1. Φάση 0 ως 1Έγινε

    Θεμέλια, πλήρες σχήμα, row-level security σε κάθε πίνακα, και οι μηχανές διαθεσιμότητας και τιμολόγησης με τα τεστ τους γραμμένα πρώτα.

  2. Φάση 2 ως 3Έγινε

    Κρατήσεις, διαθεσιμότητα και οι οθόνες του ιδιοκτήτη. Έπειτα παραλαβή και επιστροφή από άκρη σε άκρη, συμβάντα, μετρητά και παράδοση ταμείου.

  3. Φάση 4Έγινε

    Το δίγλωσσο συμφωνητικό, η υπογραφή στην οθόνη και το OCR διπλώματος με χειροκίνητη εναλλακτική που δουλεύει πάντα.

  4. Οκτώβριος 2026Μπροστά

    Δοκιμαστική έκδοση σε ένα ξενοδοχείο, παράλληλα με το χαρτί για δεκαπέντε ημέρες. Το χαρτί είναι το δίχτυ ασφαλείας, και η σύγκριση είναι το τεστ.

  5. 1 Μαρτίου 2027Μπροστά

    Έναρξη σεζόν, με τον χειμώνα αφιερωμένο σε ό,τι έδειξε η πιλοτική χρήση: αναφορές, ειδοποιήσεις και η καταχώριση στο Play Store.

Χτισμένο με

Εφαρμογή

  • Next.js App Router
  • TypeScript
  • Tailwind
  • next-intl

Δεδομένα και πρόσβαση

  • Postgres
  • Row Level Security
  • Zod

Έγγραφα και όραση

  • @react-pdf/renderer
  • Claude vision OCR
  • Canvas signature

Διάθεση

  • Railway
  • Android TWA
  • Web Push
  • Vitest

Χτίζεις κάτι αυτού του μεγέθους;

Πες μου πάνω σε τι χρειάζεται να τρέχει η επιχείρησή σου και θα επανέλθω εντός 24 ωρών με ξεκάθαρη προσφορά χωρίς δέσμευση.

Ζήτησε προσφορά