Αρχιτεκτονική
Το Stockpit είναι standalone cloud εφαρμογή - τίποτα δεν εγκαθίσταται μέσα στο WordPress εκτός από ένα προαιρετικό companion plugin. Κάθε πελάτης (tenant) έχει δικό του subdomain και δική του, πλήρως απομονωμένη βάση δεδομένων - όχι shared σχήμα με tenant_id. Τα credentials τρίτων (courier, ERP, πάροχοι τιμολόγησης) αποθηκεύονται κρυπτογραφημένα ανά tenant.
Ο συγχρονισμός με το WooCommerce στηρίζεται σε τρεις αρχές:
- Idempotent upserts: κάθε εγγραφή ταυτοποιείται από το ζεύγος (shop, woo_id). Ξαναστείλε το ίδιο webhook όσες φορές θες - δεν δημιουργεί διπλοεγγραφές.
- Webhooks + reconciliation: τα webhooks είναι το γρήγορο μονοπάτι, όχι η εγγύηση. Ανά 5 λεπτά τρέχει poller με
modified_aftercursor ανά shop που πιάνει ό,τι χάθηκε. - Echo suppression: οι αλλαγές που γράφει το Stockpit στο Woo μαρκάρονται (π.χ. pushed status), ώστε το webhook που επιστρέφει να αναγνωρίζεται ως δικό μας echo και να μην ξαναεπεξεργάζεται.
Μονάδα εργασίας στην αποθήκη δεν είναι η παραγγελία αλλά το fulfillment: μια παραγγελία μπορεί να σπάσει σε περισσότερα από ένα fulfillments αν τα προϊόντα της βρίσκονται σε διαφορετικές τοποθεσίες. Η ανάθεση γίνεται από pipeline κανόνων (διαθεσιμότητα αποθέματος, προτεραιότητα τοποθεσίας, ελαχιστοποίηση σπασιμάτων) και ό,τι δεν λύνεται αυτόματα πέφτει σε ουρά χειροκίνητης ανάθεσης.
Companion plugin (stockpit-connect)
Μικρό WordPress plugin που κατεβαίνει από τις ρυθμίσεις του tenant. Υπάρχει γιατί τα native webhooks του Woo παραδίδονται best-effort και χάνονται σιωπηλά σε timeouts. Το plugin προσθέτει:
- Υπογεγραμμένα webhooks με δική του ουρά επαναπροσπάθειας μέσα στο WordPress.
- Health endpoint για να φαίνεται η κατάσταση της σύνδεσης από το Stockpit.
- Endpoints που λείπουν από το Woo REST API - π.χ. δημιουργία παραγγελίας ως πελάτης για τηλεφωνικές πωλήσεις.
Ο receiver στο Stockpit είναι POST /webhooks/woo/{shop}. Κάθε αίτημα
επαληθεύεται με HMAC υπογραφή στο raw body (header
X-WC-Webhook-Signature, secret ανά shop) - άκυρη υπογραφή σημαίνει
401 και το payload δεν αγγίζεται. Το topic διαβάζεται από το
X-WC-Webhook-Topic (π.χ. order.updated, product.updated).
REST API v1
Κάθε tenant εκθέτει JSON API στο δικό του subdomain:
https://{tenant}.stockpit.gr/api/v1. Αυθεντικοποίηση με Sanctum
bearer tokens - είναι το ίδιο API που χρησιμοποιεί το picking PWA, οπότε ό,τι
κάνει το PWA μπορείς να το κάνεις κι εσύ.
Αυθεντικοποίηση
curl -X POST https://tokatastima.stockpit.gr/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"email":"user@example.gr","password":"...","device_name":"scanner-1"}'
# 200 → {"token":"1|xxxxxxxx..."}
# 422 → {"message":"...","errors":{"email":["..."]}}
Το device_name είναι ελεύθερο string για να ξεχωρίζεις συσκευές.
Το token δεν λήγει από μόνο του - ανακαλείται όταν αλλάξει ο κωδικός του χρήστη
ή από τη διαχείριση συνεδριών.
Endpoints
| Endpoint | Τι κάνει |
|---|---|
POST /auth/token | Εκδίδει bearer token για χρήστη του tenant |
GET /me | Στοιχεία του αυθεντικοποιημένου χρήστη |
GET /stock?barcode= | Απόθεμα ανά τοποθεσία για barcode ή SKU |
GET /picking/locations | Τοποθεσίες με εκκρεμείς εντολές συλλογής |
GET /picking/locations/{id} | Οι εντολές συλλογής της τοποθεσίας + fingerprint |
POST /picking/locations/{id}/scan | Καταχώρηση σκαναρίσματος - απορρίπτει λάθος προϊόν |
GET /gift-cards?code= | Έλεγχος δωροκάρτας και υπολοίπου |
POST /gift-cards/{code}/redeem | Εξαργύρωση ποσού - idempotent ανά request key |
Παράδειγμα: αναζήτηση αποθέματος
GET /api/v1/stock?barcode=5205551234567
Authorization: Bearer 1|xxxxxxxx...
# 200
{
"product": {"id": 12, "name": "Blue T-shirt", "sku": "TSH-BLUE-M"},
"locations": [
{"id": 1, "name": "Κεντρική αποθήκη", "code": "MAIN", "quantity": 14},
{"id": 2, "name": "Κατάστημα", "code": "SHOP", "quantity": 3}
]
}
# 404 → {"message": "Unknown code ..."} όταν το barcode/SKU δεν βρεθεί
Παράδειγμα: σκανάρισμα στη συλλογή
POST /api/v1/picking/locations/1/scan
{"code": "5205551234567"}
# 200 - το σκανάρισμα μέτρησε
{
"picked": {
"item_id": 55, "name": "Blue T-shirt - M",
"picked": 1, "quantity": 2, "fulfillment_status": "picking"
},
"fingerprint": "a1b2c3..."
}
# 404 - άγνωστο barcode | 409 - το προϊόν δεν περιμένει συλλογή εδώ
Το fingerprint είναι hash της τρέχουσας κατάστασης της τοποθεσίας.
Το PWA κάνει polling στο state endpoint και ξαναζωγραφίζει τη λίστα μόνο όταν
αλλάξει το fingerprint - κάνε το ίδιο για φθηνό realtime χωρίς websockets.
Σφάλματα και όρια
| Κωδικός | Πότε |
|---|---|
401 | Άκυρο ή ανακλημένο token / άκυρη webhook υπογραφή |
404 | Άγνωστος πόρος ή barcode |
409 | Η ενέργεια δεν επιτρέπεται στην τρέχουσα κατάσταση (π.χ. τίποτα προς συλλογή) |
422 | Λάθη επικύρωσης - δες το αντικείμενο errors |
429 | Rate limit - δες το header Retry-After |
Πρωτόκολλο print agent
Ο agent είναι ένα self-contained εκτελέσιμο (Windows, macOS, Linux, ARM) χωρίς
εξαρτήσεις. Δουλεύει μόνο με εξερχόμενο polling - δεν ανοίγει καμία θύρα, δεν
χρειάζεται port forwarding ή static IP στο κατάστημα. Κάθε εκτυπωτής παίρνει
δικό του token μορφής prn_* από τις ρυθμίσεις τοποθεσίας.
# 1. poll για δουλειές - λειτουργεί και ως heartbeat
GET /api/v1/print-agent/jobs
X-Printer-Token: prn_xxxxxxxx
# 200 → {"jobs":[{"id":41,"tracking":"7712440921","file_url":"https://..."}]}
# επιστρέφει έως 10 queued jobs, με σειρά δημιουργίας
# 2. κατέβασμα αρχείου - επιστρέφει το PDF της ετικέτας
GET /api/v1/print-agent/jobs/41/file
# 3. επιβεβαίωση αποτελέσματος
POST /api/v1/print-agent/jobs/41/ack
{"status": "printed"} # ή {"status":"failed","error":"..."}
# 200 → {"ok": true}
- Κάθε poll ενημερώνει το
last_seen_at- εκτυπωτής με σήμα μέσα στα τελευταία 30" εμφανίζεται online στο UI. - Rate limit 600 αιτήματα/λεπτό ανά token - αρκεί για polling ανά δευτερόλεπτο με περιθώριο.
- Jobs που δεν γίνονται ack παραμένουν queued - αν ο πάγκος κλείσει, τυπώνονται όταν ξανανοίξει. Τίποτα δεν χάνεται και τίποτα δεν τυπώνεται δύο φορές: το ack αλλάζει την κατάσταση οριστικά.
- Η φυσική εκτύπωση γίνεται με
lpσε macOS/Linux και SumatraPDF στα Windows. Ο επίσημος agent είναι ένα αρχείο - δες τον για να γράψεις δικόν σου σε όποια γλώσσα θες: το πρωτόκολλο είναι τα τρία παραπάνω requests.
Adapters
Κάθε εξωτερική υπηρεσία υλοποιεί ένα στενό interface και δηλώνεται σε ένα registry. Η υπόλοιπη εφαρμογή δεν ξέρει ονόματα υπηρεσιών - μιλάει μόνο στο interface:
CourierProvider createVoucher() cancelVoucher() label() track() ErpConnector pushDocument() pushTransfer() testConnection() InvoiceProvider issue() # επιστρέφει external id, ΜΑΡΚ, PDF url
- Couriers: ACS, Γενική Ταχυδρομική, BoxNow - το Speedex σε beta. Καθένας κουβαλάει τα δικά του credentials fields, το UI τα ζωγραφίζει δυναμικά.
- ERP: SoftOne (s1services) και Galaxy, με mapping τρόπων πληρωμής και αποθηκών σε κωδικούς του ERP.
- Τιμολόγηση: Elorus και Oxygen Πελατολόγιο - ο πάροχος εκδίδει το παραστατικό και το διαβιβάζει στο myDATA, εμείς κρατάμε id, ΜΑΡΚ και PDF.
Κάθε εξερχόμενο έγγραφο (ERP ή τιμολόγηση) περνάει από dead-letter κύκλο ζωής:
pending → pushed/issued ή failed με αποθηκευμένο το
μήνυμα λάθους και μετρητή προσπαθειών. Το failed ξαναστέλνεται από το UI με ένα
κλικ, και νυχτερινό σάρωμα εντοπίζει ό,τι ξέμεινε. Νέα διασύνδεση σημαίνει ένα
class - όχι αλλαγές στη ροή.
Ασφάλεια και όρια
- Απομονωμένη βάση ανά tenant - ένα SQL injection σε άλλον tenant δεν φτάνει στα δικά σου δεδομένα ούτε θεωρητικά.
- Κρυπτογραφημένα credentials με encrypted casts - δεν υπάρχουν plaintext κλειδιά στη βάση.
- Υπογεγραμμένα webhooks (HMAC στο raw body) και rate limiting σε όλα τα public endpoints.
- Τα tokens του API είναι per-user - η ανάκληση χρήστη ανακαλεί και τις συσκευές του.
- Adapters σε beta (π.χ. Speedex) επαληθεύονται με τα credentials του tenant σε πραγματική δοκιμή πριν ενεργοποιηθούν πλήρως.
Θες κάτι που δεν καλύπτεται;
Αν στήνεις κάτι πάνω στο Stockpit και χρειάζεσαι επιπλέον endpoint, webhook προς τα έξω ή adapter για υπηρεσία που δεν υποστηρίζουμε, γράψε μας στο hello@stockpit.gr - το API μεγαλώνει με βάση πραγματικές ανάγκες, και τα αιτήματα από devs μπαίνουν ψηλά στη λίστα.