הטמעת מערכת הזמנות להשכרת רכבים וציוד באתר שלך
לקוח מגיע עם בקשה: "אני רוצה להשכיר אופנועי שטח כך שהלקוח יוכל לבחור תאריך ולהזמין באינטרנט." במבט ראשון, זו הזמנה טיפוסית כמו מלון. אבל השכרה שונה: החפץ פיזית זז (הרכב נוסע), משך ההשכרה יכול להיות לפי שעה, נדרש פיקדון, וההחזרה יכולה להיות במיקום אחר. נתקלנו בזה עשרות פעמים — בואו נספר לכם איך לתכנן מערכת ללא כאבים.
אנחנו מתמחים במערכות השכרת רכבים וציוד עם תמחור לפי שעה, טיפול בפיקדונות ומעקב אחר החזרות באיחור. המערכת שלנו חוסכת ללקוחות בממוצע 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 ימי עבודה בהתאם למורכבות.
צרו קשר כדי לדון במשימה שלכם — נכין הצעה מסחרית ללא עלות. או הזמינו ביקורת מקדימה של הפתרון הנוכחי שלכם.







