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