feat: sistem de linkuri publice pentru proprietăți
Expune fluxul de rezervare anonimă printr-un link public partajabil
(/book/<slug>), cu calendar public, cod QR și proprietate demo publică.
Backend:
- slug + list_on_landing pe Property; utilitar slugify (diacritice RO,
cuvinte rezervate, ban all-digit) + migrare idempotentă raw-DDL rulată
din entrypoint; SQLite WAL + busy_timeout
- GET /public/properties/{slug_or_id}(+/spaces): rezolvare slug/ID, 404 uniform
- GET /public/spaces/{id}/busy: doar {start_time,end_time} (fără PII),
pending+approved, tz-aware/interval>60z → 422, orfan/privat → 404
- availability: scrub user_name/title (fix scurgere PII)
- POST /public/bookings: overlap pending+approved, cap 5/email/spațiu/zi,
rate-limit per-IP, suprimare notificări+email pe demo, email confirmare guest
- PATCH slug (409 duplicat / 422 format / 403 non-manager); guest_email EmailStr
- email guest la aprobare/respingere cu cod #RB-<id>
- teste noi: test_public.py + test_migrate_slug.py (33 teste)
Frontend:
- rută /book/:slug; PublicBooking rescris cu calendar grid manual
(PublicCalendar + GuestBookingForm), 30min, 08-20, mobil day-view, RO
- card „Link public" în PropertyDetail: copy, edit slug, QR pe URL-ul cu ID
- secțiune proprietăți publice + CTA pe Landing; buton copy în Properties
Gate aprobat (U1-U3, G1 grid manual, G2 list_on_landing); G3-G5 în TODOS.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"""Booking schemas for request/response."""
|
||||
from datetime import datetime, date
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
|
||||
|
||||
|
||||
class BookingCalendarPublic(BaseModel):
|
||||
@@ -276,5 +276,5 @@ class AnonymousBookingCreate(BaseModel):
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
description: str | None = None
|
||||
guest_name: str = Field(..., min_length=1)
|
||||
guest_email: str = Field(..., min_length=1)
|
||||
guest_email: EmailStr
|
||||
guest_organization: str | None = None
|
||||
|
||||
@@ -9,6 +9,7 @@ class PropertyCreate(BaseModel):
|
||||
description: str | None = None
|
||||
address: str | None = None
|
||||
is_public: bool = True
|
||||
list_on_landing: bool = False
|
||||
|
||||
|
||||
class PropertyUpdate(BaseModel):
|
||||
@@ -16,6 +17,8 @@ class PropertyUpdate(BaseModel):
|
||||
description: str | None = None
|
||||
address: str | None = None
|
||||
is_public: bool | None = None
|
||||
slug: str | None = None
|
||||
list_on_landing: bool | None = None
|
||||
|
||||
|
||||
class PropertyManagerInfo(BaseModel):
|
||||
@@ -34,6 +37,9 @@ class PropertyResponse(BaseModel):
|
||||
created_at: datetime
|
||||
space_count: int = 0
|
||||
managers: list[PropertyManagerInfo] = []
|
||||
slug: str | None = None
|
||||
list_on_landing: bool = False
|
||||
require_approval: bool | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user