BusiKM — Schemat bazy danych

1. Przegląd strategii bazodanowej

BusiKM korzysta z trzech silników przechowywania danych, dobranych pod kątem specyfiki obciążeń:

SilnikZastosowanieUzasadnienie
PostgreSQLDane transakcyjne, modele domenoweACID, relacje, constrainty, migracje Django ORM
MongoDBPunkty GPS (dane szeregowe, duże wolumeny)Elastyczny schemat, indeks 2dsphere, TTL, sharding
RedisCache, kolejka zadań, blacklist tokenów JWTOperacje in-memory, TTL, atomowe liczniki

Wszystkie modele PostgreSQL posiadają pola created_at i updated_at (auto_now_add / auto_now).


2. Diagram ERD (uproszczony)

Company ──1──1── Subscription
   │          └──1──1── AccountingFirm ──1──*── AccountingFirmClient
   │                                                  └── FK → Company (client)
   │
   ├──1──*── User (role: driver/owner/accountant/accounting_firm)
   │            └──1──1── Driver
   │                        ├──1──*── DriverDocument
   │                        └──FK── default_vehicle → Vehicle
   │
   ├──1──*── Vehicle
   │            └──1──*── VehicleDocument
   │
   ├──1──*── Trip
   │            ├──FK── driver, vehicle
   │            ├──1──*── TripClassificationLog
   │            └──1──*── OdometerPhoto
   │
   ├──1──*── Alert ──FK→ Vehicle?, Driver?, VehicleDocument?
   │
   ├──1──*── MileageReport, FleetCostReport, DelegationReport
   │
   ├──1──*── ExportRecord ──1──*── ExportStatusLog
   │
   ├──1──*── Invitation
   │
   └──1──*── UsageSnapshot

User ──1──*── DevicePushToken
Subscription ──1──*── TrialEmailLog

3. PostgreSQL — modele szczegółowo

3.1 User (app: accounts)

Dziedziczy po AbstractUser. Logowanie przez email (USERNAME_FIELD = 'email').

PoleTypOgraniczenia
emailEmailFieldunique, indeks, pole loginu
first_nameCharField(150)
last_nameCharField(150)
roleCharField — choicesdriver / owner / accountant / accounting_firm
phoneCharField(20)opcjonalne
companyFK -> Companynullable (accounting_firm bez company)
is_activeBooleanFielddefault True
is_email_verifiedBooleanFielddefault False
token_versionIntegerFielddefault 0, inkrementacja = unieważnij tokeny
date_joinedDateTimeFieldauto_now_add

3.2 Company (app: companies)

Soft delete — manager objects zwraca tylko nieusunięte, all_with_deleted zwraca wszystkie.

PoleTypOgraniczenia
nameCharField(255)wymagane
nipCharField(13)unique (warunkowo, gdzie != '')
regonCharField(14)opcjonalne
krsCharField(14)opcjonalne
streetCharField(255)opcjonalne
cityCharField(100)opcjonalne
postal_codeCharField(10)opcjonalne
countryCharField(100)default 'PL'
phoneCharField(20)opcjonalne
emailEmailFieldopcjonalne
websiteURLFieldopcjonalne
license_numberCharField(50)numer licencji transportowej
fleet_sizeIntegerFieldzdenormalizowane, default 0
is_activeBooleanFielddefault True
is_deletedBooleanFielddefault False
deleted_atDateTimeFieldnullable

3.3 Subscription (app: companies)

PoleTypOgraniczenia
companyOneToOneFieldFK -> Company, on_delete CASCADE
planCharFieldfree / starter / professional / enterprise
statusCharFieldtrial / pilot / af_trial / af_client_trial / active / expired / cancelled
valid_untilDateTimeFieldnullable
trial_typeCharFieldnullable
trial_started_atDateTimeFieldnullable
downgraded_atDateTimeFieldnullable
previous_planCharFieldnullable
max_vehiclesIntegerField
max_driversIntegerField
featuresJSONFielddefault dict, elastyczny zbiór funkcji planu

3.4 AccountingFirm (app: companies)

PoleTypOgraniczenia
companyOneToOneFieldFK -> Company
max_clientsIntegerField
tax_officeCharFieldopcjonalne
accountant_licenseCharFieldopcjonalne
specializationsJSONFieldlista specjalizacji
onboarding_completedBooleanFielddefault False
trial_min_clientsIntegerFieldminimum klientów w trialu
trial_condition_metBooleanFielddefault False

3.5 AccountingFirmClient (app: companies)

PoleTypOgraniczenia
accounting_firmFK-> AccountingFirm
client_companyFK-> Company
is_activeBooleanFielddefault True
access_levelCharFieldfull / read_only / reports_only
contract_startDateFieldnullable
contract_endDateFieldnullable
notesTextFieldopcjonalne
added_byFK-> User

Unique constraint: (accounting_firm, client_company)

3.6 Vehicle (app: fleet)

Soft delete. Unique constraint na (company, plate_number) gdzie is_deleted = False.

PoleTypOgraniczenia
companyFK-> Company
plate_numberCharField(20)
brandCharField(50)
modelCharField(50)
yearIntegerFieldrok produkcji
vinCharField(17)opcjonalne
vehicle_typeCharFieldtruck / van / car / bus / semi_trailer / trailer
fuel_typeCharFielddiesel / petrol / lpg / cng / electric / hybrid
mileage_kmIntegerFieldaktualny przebieg
engine_capacityIntegerFieldcm3, opcjonalne
engine_power_kwIntegerFieldopcjonalne
max_weight_kgIntegerFieldDMC, opcjonalne
euro_classCharFieldnorma emisji spalin
statusCharFieldactive / in_service / decommissioned
notesTextFieldopcjonalne
is_deletedBooleanFielddefault False
deleted_atDateTimeFieldnullable

3.7 VehicleDocument (app: fleet)

PoleTypOgraniczenia
vehicleFK-> Vehicle
document_typeCharFieldoc_insurance / ac_insurance / registration / technical_inspection / tachograph_certificate / transport_license / lease_agreement / other
titleCharField
fileFileFieldstorage: S3
file_nameCharField
file_sizeIntegerFieldw bajtach
content_typeCharFieldMIME type
issue_dateDateFieldnullable
expiry_dateDateFieldnullable
insurerCharFieldopcjonalne, dotyczy polis
policy_numberCharFieldopcjonalne, dotyczy polis
costDecimalFieldnullable
notesTextFieldopcjonalne
uploaded_byFK-> User
is_deletedBooleanFielddefault False
deleted_atDateTimeFieldnullable

3.8 Driver (app: drivers)

PoleTypOgraniczenia
userOneToOneField-> User
companyFK-> Company
employee_numberCharFieldopcjonalne
license_numberCharFieldunique (warunkowo, != '')
license_categoriesJSONFieldlista, np. ["C", "CE", "D"]
license_expiry_dateDateFieldnullable
license_issued_byCharFieldopcjonalne
qualification_certificateCharFieldnumer świadectwa kwalifikacji
qualification_expiry_dateDateFieldnullable
medical_exam_expiry_dateDateFieldnullable
psychological_exam_expiry_dateDateFieldnullable
tachograph_card_numberCharFieldopcjonalne
tachograph_card_expiryDateFieldnullable
default_vehicleFK-> Vehicle, nullable
statusCharFieldactive / on_leave / suspended / inactive
hire_dateDateFieldnullable
notesTextFieldopcjonalne
is_deletedBooleanFielddefault False
deleted_atDateTimeFieldnullable

3.9 DriverDocument (app: drivers)

PoleTypOgraniczenia
driverFK-> Driver
document_typeCharFielddriving_license / qualification_certificate / medical_exam / psychological_exam / tachograph_card / id_card / employment_contract / training_certificate / other
titleCharField
fileFileFieldstorage: S3
file_nameCharField
file_sizeIntegerField
content_typeCharField
document_numberCharFieldopcjonalne
issued_byCharFieldopcjonalne
issue_dateDateFieldnullable
expiry_dateDateFieldnullable
notesTextFieldopcjonalne
uploaded_byFK-> User
is_deletedBooleanFielddefault False
deleted_atDateTimeFieldnullable

3.10 Invitation (app: drivers)

PoleTypOgraniczenia
companyFK-> Company
emailEmailField
roleCharFieldrola przypisywana po akceptacji
invited_byFK-> User
tokenCharFieldunique, UUID
statusCharFieldpending / accepted / expired / cancelled / declined
messageTextFieldopcjonalne, wiadomość od zapraszającego
accepted_byFK-> User, nullable
accepted_atDateTimeFieldnullable
expires_atDateTimeField
resent_countIntegerFielddefault 0
last_sent_atDateTimeFieldnullable

3.11 Trip (app: trips)

PoleTypOgraniczenia
companyFK-> Company
driverFK-> Driver
vehicleFK-> Vehicle
trip_typeCharFieldbusiness / private
statusCharFieldin_progress / completed / cancelled
start_timeDateTimeField
end_timeDateTimeFieldnullable
start_location_nameCharFieldopcjonalne
end_location_nameCharFieldopcjonalne
start_latitudeDecimalFieldnullable
start_longitudeDecimalFieldnullable
end_latitudeDecimalFieldnullable
end_longitudeDecimalFieldnullable
start_odometer_kmIntegerFieldnullable
end_odometer_kmIntegerFieldnullable
distance_kmDecimalFieldnullable
distance_sourceCharFieldgps / odometer / manual
duration_minutesIntegerFieldnullable
purposeTextFieldopcjonalne
route_descriptionTextFieldopcjonalne
gps_points_countIntegerFielddefault 0
notesTextFieldopcjonalne
original_trip_typeCharFieldnullable, przed reklasyfikacją
reclassified_atDateTimeFieldnullable
reclassified_byFK-> User, nullable
reclassification_reasonTextFieldnullable

3.12 TripClassificationLog (app: trips)

PoleTypOgraniczenia
tripFK-> Trip
from_typeCharFieldbusiness / private
to_typeCharFieldbusiness / private
changed_byFK-> User
reasonTextField

3.13 OdometerPhoto (app: trips)

PoleTypOgraniczenia
tripFK-> Trip
photo_typeCharFieldstart / end
fileImageFieldstorage: S3
file_nameCharField
file_sizeIntegerField
content_typeCharField
odometer_value_kmIntegerFieldnullable
latitudeDecimalFieldnullable
longitudeDecimalFieldnullable
taken_atDateTimeFieldnullable
uploaded_byFK-> User

Unique constraint: (trip, photo_type)

3.14 Alert (app: fleet)

PoleTypOgraniczenia
companyFK-> Company
alert_typeCharFieldoc_expiring / ac_expiring / inspection_expiring / tachograph_expiring / license_expiring / driver_license_expiring / qualification_expiring / medical_exam_expiring / psychological_exam_expiring / tachograph_card_expiring / custom
severityCharFieldinfo / warning / critical / expired
statusCharFieldactive / acknowledged / resolved / dismissed
titleCharField
messageTextField
vehicleFK-> Vehicle, nullable
driverFK-> Driver, nullable
documentFK-> VehicleDocument, nullable
expiry_dateDateFieldnullable
days_remainingIntegerFieldnullable
acknowledged_atDateTimeFieldnullable
acknowledged_byFK-> User, nullable
resolved_atDateTimeFieldnullable

Unique constraint: (document, alert_type) WHERE status IN ('active', 'acknowledged')

3.15 MileageReport (app: documents)

PoleTypOgraniczenia
companyFK-> Company
vehicleFK-> Vehicle
period_startDateField
period_endDateField
generated_byFK-> User
statusCharFieldgenerating / completed / failed
fileFileFieldstorage: S3
file_nameCharField
file_sizeIntegerFieldnullable
trips_countIntegerFielddefault 0
total_distance_kmDecimalField
total_amountDecimalFieldkwota za przebieg
rate_per_kmDecimalFieldstawka za km
start_odometer_kmIntegerFieldnullable
end_odometer_kmIntegerFieldnullable
metadataJSONFielddefault dict
error_messageTextFieldnullable
generated_atDateTimeFieldnullable

3.16 FleetCostReport (app: documents)

Struktura analogiczna do MileageReport, z dodatkowymi polami:

Pole dodatkoweTypOgraniczenia
pdf_fileFileFieldstorage: S3
csv_fileFileFieldstorage: S3
vehicles_countIntegerField
total_costDecimalField
cost_per_kmDecimalField

3.17 DelegationReport (app: documents)

Struktura analogiczna, z polami specyficznymi dla delegacji:

Pole dodatkoweTypOgraniczenia
driverFK-> Driver
report_typeCharFieldindividual / summary
total_tripsIntegerField
total_work_hoursDecimalField
total_allowanceDecimalFielddieta
total_accommodationDecimalFieldkoszty noclegu
total_costDecimalField

3.18 ExportRecord (app: integrations)

PoleTypOgraniczenia
companyFK-> Company
integration_nameCharFieldnp. optima, symfonia
export_typeCharField
period_startDateField
period_endDateField
vehicleFK-> Vehicle, nullable
driverFK-> Driver, nullable
statusCharField
fileFileFieldstorage: S3
file_nameCharField
file_sizeIntegerFieldnullable
records_countIntegerFielddefault 0
total_amountDecimalFieldnullable
total_distance_kmDecimalFieldnullable
warningsJSONFielddefault list
error_messageTextFieldnullable
metadataJSONFielddefault dict
generated_byFK-> User
generated_atDateTimeFieldnullable

3.19 ExportStatusLog (app: integrations)

PoleTypOgraniczenia
export_recordFK-> ExportRecord
from_statusCharField
to_statusCharField
messageTextFieldopcjonalne

3.20 DevicePushToken (app: notifications)

PoleTypOgraniczenia
userFK-> User
push_tokenCharField
platformCharFieldios / android
device_nameCharFieldopcjonalne
is_activeBooleanFielddefault True

Unique constraint: (user, push_token)

3.21 UsageSnapshot (app: common)

PoleTypOgraniczenia
companyFK-> Company
monthCharField(7)format YYYY-MM
vehicles_activeIntegerFielddefault 0
drivers_activeIntegerFielddefault 0
trips_completedIntegerFielddefault 0
trips_businessIntegerFielddefault 0
distance_total_kmDecimalFielddefault 0
gps_points_ingestedIntegerFielddefault 0
reports_generatedIntegerFielddefault 0
exports_generatedIntegerFielddefault 0
logins_countIntegerFielddefault 0
active_daysIntegerFielddefault 0
mobile_sessionsIntegerFielddefault 0
web_sessionsIntegerFielddefault 0
snapshotted_atDateTimeField

Unique constraint: (company, month)

3.22 TrialEmailLog (app: billing)

PoleTypOgraniczenia
subscriptionFK-> Subscription
email_typeCharFieldnp. trial_ending_7d
sent_atDateTimeFieldauto_now_add
recipient_emailEmailField

Unique constraint: (subscription, email_type)


4. MongoDB — kolekcje i indeksy

Kolekcja gps_points

Przechowuje surowe punkty GPS rejestrowane przez aplikację mobilną podczas trasy.

{
  "trip_id":          "ObjectId / UUID",
  "company_id":       "int",
  "driver_id":        "int",
  "vehicle_id":       "int",
  "location":         { "type": "Point", "coordinates": [lng, lat] },
  "latitude":         20.123456,
  "longitude":        50.654321,
  "altitude":         230.5,
  "speed":            65.2,
  "bearing":          180.0,
  "accuracy":         4.5,
  "timestamp":        "ISODate",
  "server_timestamp": "ISODate",
  "battery_level":    82,
  "is_moving":        true,
  "source":           "gps / network / fused",
  "batch_id":         "UUID"
}

Indeksy:

IndeksTypCel
location2dsphereZapytania geospatial (np. trasa na mapie)
(trip_id, timestamp)compoundPobranie punktów trasy w kolejności
timestampTTL (90 dni)Automatyczne usuwanie starych danych
batch_idsingleIdempotentność przy ponownym wysyłaniu
(company_id, vehicle_id, timestamp)compoundZapytania raportowe po firmie/pojeździe

5. Redis — logiczne bazy danych

DBPrzeznaczeniePrzykładowe klucze
0Cache API, liczniki użyciacache:company:{id}:vehicles, usage:{company}:{month}:trips
1Celery brokerKolejki zadań: generowanie raportów, alerty, eksporty
2Blacklist tokenów JWT, token familiestoken_blacklist:{jti}, token_family:{family_id}

Klucze w DB 0 korzystają z TTL dostosowanego do typu danych (np. 5 min dla list pojazdów, 1h dla raportów). DB 2 używa TTL równego czasowi wygaśnięcia tokena refresh.


6. Relacje i klucze obce

Kluczowe relacje w systemie:

  • Company jest centralnym węzłem — większość modeli posiada FK do Company (izolacja danych tenant).
  • User -> Company: FK, nullable (konta biur rachunkowych mogą nie mieć przypisanej firmy transportowej).
  • Driver -> User: OneToOne — każdy kierowca jest powiązany z kontem użytkownika.
  • Subscription -> Company: OneToOne — każda firma ma dokładnie jedną subskrypcję.
  • AccountingFirm -> Company: OneToOne — firma księgowa to rozszerzenie encji Company.
  • AccountingFirmClient: tabela pośrednia łącząca biuro rachunkowe z firmami-klientami.
  • Trip -> Driver, Vehicle: FK — każda trasa przypisana do kierowcy i pojazdu.
  • Alert -> Vehicle, Driver, VehicleDocument: nullable FK — alert może dotyczyć pojazdu, kierowcy lub dokumentu.

Wszystkie klucze obce używają on_delete=CASCADE lub on_delete=SET_NULL (dla pól nullable), zgodnie z logiką biznesową.


7. Indeksy i constrainty

Unique constraints warunkowe (partial unique index)

-- NIP firmy unikalny tylko gdy niepusty
CREATE UNIQUE INDEX uq_company_nip ON companies_company (nip) WHERE nip != '';

-- Numer rejestracyjny unikalny w ramach firmy (aktywne pojazdy)
CREATE UNIQUE INDEX uq_vehicle_plate ON fleet_vehicle (company_id, plate_number)
    WHERE is_deleted = FALSE;

-- Numer prawa jazdy kierowcy unikalny gdy niepusty
CREATE UNIQUE INDEX uq_driver_license ON drivers_driver (license_number)
    WHERE license_number != '';

-- Jeden aktywny alert na dokument danego typu
CREATE UNIQUE INDEX uq_alert_doc_type ON fleet_alert (document_id, alert_type)
    WHERE status IN ('active', 'acknowledged');

Indeksy wydajnościowe

  • Trip: indeks na (company_id, start_time) — filtrowanie tras po dacie.
  • Trip: indeks na (driver_id, status) — aktywne trasy kierowcy.
  • Alert: indeks na (company_id, status, severity) — dashboard alertów.
  • UsageSnapshot: indeks na (company_id, month) — unique + szybkie odczyty.

8. Wzorzec soft delete

Modele z soft delete: Company, Vehicle, VehicleDocument, Driver, DriverDocument.

class SoftDeleteManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_deleted=False)

class SoftDeleteModel(models.Model):
    is_deleted = models.BooleanField(default=False)
    deleted_at = models.DateTimeField(null=True, blank=True)

    objects = SoftDeleteManager()          # domyślny -- bez usuniętych
    all_with_deleted = models.Manager()    # wszystkie rekordy

    def soft_delete(self):
        self.is_deleted = True
        self.deleted_at = timezone.now()
        self.save(update_fields=['is_deleted', 'deleted_at'])

    class Meta:
        abstract = True

Rekord oznaczony jako usunięty:

  • Nie pojawia się w domyślnych querysetach (objects).
  • Zachowuje integralność referencyjną (FK nie są kasowane).
  • Może zostać przywrócony przez admina.
  • Unique constraints uwzględniają flagę is_deleted (partial index).

9. Migracje — konwencje

  • Każda aplikacja Django (accounts, companies, fleet, drivers, trips, documents, integrations, notifications, common, billing) zarządza własnymi migracjami.
  • Migracje generowane przez python manage.py makemigrations — nie edytowane ręcznie, chyba że wymagana jest migracja danych (RunPython).
  • Migracje danych (data migrations) stosowane do: seedowania planów subskrypcji, migracji istniejących rekordów po zmianie schematu, wypełniania pól zdenormalizowanych.
  • Środowisko CI uruchamia python manage.py migrate --check aby wykryć brakujące migracje.
  • Nazewnictwo: automatyczne (np. 0001_initial, 0002_add_fleet_size). Migracje danych nazywane opisowo: 0003_populate_fleet_size.