Property Listing CRUD
Summary
Pros manage their own resale/rental property listings through a standard CRUD API. All routes require authentication and enforce ownership — a Pro can only read, update, delete, or add photos to their own properties. Listing creation generates a URL slug from the title. Photo uploads are processed by Sharp (resized to 1200px max, converted to WebP).
State Diagram
stateDiagram-v2 state "Brouillon" as Draft state "Annonce active" as Active state "Vendu" as Sold state "Supprimé" as Deleted state "Photos ajoutées" as PhotosAdded [*] --> Draft: pro creates listing Draft --> Active: pro activates listing Active --> Sold: pro marks listing as sold Active --> Draft: pro reverts listing to draft Draft --> Deleted: pro deletes listing Active --> Deleted: pro deletes listing Sold --> Deleted: pro deletes listing Active --> PhotosAdded: pro uploads photos PhotosAdded --> Active Deleted --> [*]
Steps
1. List Properties (Actor: pro)
GET /pro/properties. Returns the authenticated pro’s properties ordered by createdAt DESC, id DESC. Supports cursor-based pagination (compound cursor (createdAt, id) base64url-encoded) with a limit query parameter.
Returns { data, cursor: { next }, hasMore }.
Triggers: Pro opens their property dashboard Outcome: Paginated list of own properties
2. Create Property (Actor: pro)
POST /pro/properties with body validated by createPropertySchema. Required fields include title, type (appartement, maison, terrain, commerce, bureau), transactionType (vente, location), city, postalCode.
A URL slug is generated from the title via generateSlug (shared utility). The property is created with proId: request.proId and source: "manual" (distinguishes from feed-imported properties). Default status is draft unless specified.
Returns the created property with HTTP 201.
Triggers: Pro fills out listing form and submits Outcome: Property row inserted; slug generated
3. Update Property (Actor: pro)
PUT /pro/properties/:id with body validated by updatePropertySchema.
Ownership check: prisma.property.findFirst({ where: { id, proId: request.proId } }). If not found (or belongs to another pro) → 404 NOT_FOUND.
Only explicitly named fields are written via destructuring — proId, slug, and source cannot be overwritten through this endpoint.
Updatable fields: title, description, type, transactionType, price, surface, rooms, bedrooms, bathrooms, floor, totalFloors, buildingYear, energyRating, gesRating, address, city, postalCode, department, region, latitude, longitude, photos, status.
Triggers: Pro edits a listing Outcome: Updated property returned
4. Delete Property (Actor: pro)
DELETE /pro/properties/:id. Ownership check same as update. Deletes the row. Returns HTTP 204 No Content.
Note: deleting a property does not automatically clean up uploaded photo files from disk.
Triggers: Pro removes a listing Outcome: Property row deleted
5. Upload Photos (Actor: pro)
POST /pro/properties/:id/photos (multipart). Ownership check same as update.
Iterates over all files in the multipart stream. Each file buffer is passed to saveUploadedImage(buffer, mimetype, propertyId) — Sharp resizes to max 1200px and converts to WebP 80% quality. The resulting relative path is collected. New paths are appended to the existing photos JSON array and saved.
Returns the updated property object.
Triggers: Pro uploads listing photos
Outcome: Photos stored on disk; photos array updated in DB
Error States
- Property not found or owned by different pro → 404
NOT_FOUND(create: n/a; update/delete/photos: applies) - Schema validation failure → 400 (Fastify JSON schema error)
- Unauthenticated → 401 (authenticatePro hook)
- CNI gate active → 403
CNI_REQUIRED
Related Processes
- contact-request-management — contact requests are associated with properties
- registration — pro must exist before managing listings