{"openapi":"3.0.3","info":{"title":"Odealz API","version":"1.0.0","description":"API petites annonces Odealz.\n\n## Catégories\n\n- GET /categories retourne l’arbre des catégories actives, ordonné pour les sélecteurs d’annonce.\n- Une annonce doit choisir une **sous-catégorie** (feuille), pas une catégorie racine.\n- Les catégories réservées au commercial sont exclues pour les annonces non commerciales et lorsque le mode commercial est désactivé.\n- GET /listings?categorySlug=... filtre une catégorie et tous ses descendants.\n- L’administration peut créer, modifier et désactiver une catégorie via /admin/categories ; la désactivation inclut ses sous-catégories et conserve les annonces existantes.\n- Les annonces peuvent préciser l’état du produit : NEW, LIKE_NEW, GOOD, FAIR ou FOR_PARTS. Le champ est facultatif, notamment pour les annonces de services.\n\n## Médias, confiance et cycle de vie\n\n- Les annonceurs peuvent ajouter jusqu’à 10 images JPEG, PNG ou WebP (8 Mo par image), les réordonner et les supprimer. Les fichiers ne deviennent accessibles publiquement que pour une annonce publiée et non expirée.\n- Les photos sont réencodées et les métadonnées sont retirées avant stockage.\n- Les adresses utilisent les provinces et territoires ainsi que les codes postaux canadiens. Rue, unité, code postal et coordonnées restent privés. La recherche par rayon est limitée à 100 km.\n- Les photos sont stockées dans un bucket S3 privé en production, avec versionnage activé ; le stockage local reste destiné au développement.\n- Les annonces expirent après 30 jours et peuvent être renouvelées par leur propriétaire. Le catalogue ignore les annonces dont l’échéance est passée.\n- Les utilisateurs connectés peuvent signaler une annonce ou un compte, et bloquer/débloquer un compte. L’administration peut traiter les signalements, retirer une annonce ou suspendre un compte.\n- Les limites anti-abus sont stockées de manière atomique dans PostgreSQL et partagées entre les instances API.\n- Après une remise terminée, seul l’intéressé peut attribuer une note de 1 à 5 à l’annonceur. Une seule note est acceptée par remise ; la réputation publique expose uniquement une moyenne, un total et une distribution, sans commentaire ni identité du déposant.\n- Un compte peut télécharger ses données au format JSON et demander sa suppression en confirmant son mot de passe. La suppression efface également les annonces et conversations liées par les règles de cascade PostgreSQL.\n\n## Accès\n\n- **Routes métier** : préfixe `/api/v1`, authentification **JWT Bearer** lorsque indiqué.\n- **Documentation** : `/` (interface visuelle), `/openapi.json` (schéma OpenAPI) et `/postman.json` (collection Postman v2.1), accessibles sans authentification. Les routes `/api/v1` privées restent protégées par JWT Bearer. `GET /health` est public.\n- **Healthcheck** : `GET /health` (hors version, sans authentification). Le navigateur reçoit un tableau HTML ; `?format=json` ou `Accept: application/json` renvoie le rapport JSON. Chaque ligne indique si le service est configuré dans l’environnement et s’il répond.\n\n## Mode commercial (`COMMERCIAL_FEATURES_ENABLED`)\n\nLorsque la variable vaut `false` :\n\n- Les annonces **COMMERCIAL** sont masquées du catalogue public et des favoris.\n- Création / modification d’annonces commerciales refusée.\n- Historique de prix visible uniquement par le **propriétaire** de l’annonce.\n- Alertes favori `notifyPriceChange` refusées.\n- **Nouvelles** conversations sur annonces commerciales refusées ; les conversations déjà ouvertes restent utilisables (messages et remises).\n\n## Statuts d’annonce\n\n- Création : passage direct à **PUBLISHED** (pas de brouillon via l’API actuelle).\n- **RESERVED** : lors de la **confirmation** d’une remise (`confirm`), si l’annonce était **PUBLISHED** (transition atomique).\n- **COMPLETED** : lors du marquage **complete** d’une remise confirmée.\n- **ARCHIVED** : via `POST /listings/{id}/archive` (propriétaire) ; idempotent si déjà archivée.\n\n## Remises (pickup)\n\nStatuts : **PROPOSED** → **CONFIRMED** | **CANCELLED** (refus) ; **CONFIRMED** → **COMPLETED** | **CANCELLED** (annulation par le proposeur) ; **PROPOSED** → **CANCELLED** (annulation par le proposeur).\n\n| Action | Qui | Effet annonce |\n| ------ | --- | ------------- |\n| `confirm` | Destinataire (non proposeur) | **PUBLISHED** → **RESERVED** si succès |\n| `reject` | Destinataire | Remise **CANCELLED** |\n| `cancel` | Proposeur (proposition ou remise confirmée) | **CANCELLED** ; si était confirmée, annonce **RESERVED** → **PUBLISHED** |\n| `complete` | Participant | Remise **COMPLETED**, annonce **COMPLETED** |\n\nLes champs de lieu de remise ne sont renvoyés qu’aux **participants** de la conversation.\n\n## Chat (conversations)\n\n- **Liste** : `GET /me/conversations` — dernier message et `unreadCount`.\n- **Fil** : `GET /conversations/{id}/messages` sans `page` — fenêtre des derniers messages ; `before` / `after` pour historique et polling.\n- **Lu** : `POST /conversations/{id}/read` — curseur de lecture par participant.\n- Temps réel : polling `?after=` ; WebSocket/SSE en évolution.\n\n## Notifications\n\nPersistées par l’API (`PENDING` à la création) ; envoi e-mail/SMS par le **worker** (`SENT` / `FAILED`). Les préférences utilisateur et le canal déterminent les lignes créées. **Aucune garantie « exactement une fois »** côté fournisseur (retry possible après acceptation).\n\n`GET /me/notifications` : historique du destinataire connecté uniquement ; payload public sans secrets ni contenu privé des messages.\n\n## Recherches enregistrées\n\n- `POST /me/saved-searches` enregistre une alerte par catégorie (sous-catégories incluses), ville/pays, type et plage de prix/devise.\n- La publication d’une annonce déclenche un fanout pg-boss par lots. Les notifications suivent les préférences e-mail/SMS activées du compte.\n- Un compte peut enregistrer jusqu’à 20 recherches, les suspendre ou les supprimer. Les recherches commerciales respectent `COMMERCIAL_FEATURES_ENABLED`.\n\n## Récupération de compte\n\n- `POST /auth/password-reset/request` répond de façon identique que l’adresse existe ou non.\n- `POST /auth/password-reset/confirm` vérifie un code à usage unique, change le mot de passe et révoque les jetons de rafraîchissement.\n- Le code expire après 15 minutes. L’envoi nécessite `ACCOUNT_CODE_ENCRYPTION_KEY`, identique dans le backend et le worker.\n- Les coordonnées ne sont pas obligatoirement vérifiées pour utiliser l’API. Les vérifications peuvent être demandées depuis une session authentifiée."},"servers":[{"url":"/api/v1","description":"API versionnée (routes métier)"},{"url":"/","description":"Racine (healthcheck uniquement dans ce schéma)"}],"tags":[{"name":"Auth","description":"Inscription, connexion, jetons"},{"name":"Profil","description":"Compte et préférences"},{"name":"Recherches enregistrées","description":"Alertes de nouvelles annonces"},{"name":"Catégories","description":"Arbre des catégories disponibles"},{"name":"Confiance et sécurité","description":"Signalements, blocages et modération"},{"name":"Annonces","description":"Catalogue, CRUD annonceur, prix"},{"name":"Favoris","description":"Favoris et alertes prix"},{"name":"Conversations","description":"Messages et remises"},{"name":"Notifications","description":"Historique des notifications"},{"name":"Système","description":"Santé du service (hors /api/v1)"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Jeton d’accès obtenu via `/auth/login` ou `/auth/register`."}},"parameters":{"PageQuery":{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},"LimitQuery":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},"ListingIdPath":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"ConversationIdPath":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}},"responses":{"Unauthorized":{"description":"JWT absent ou invalide","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"Droits insuffisants ou mode commercial","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Ressource introuvable ou non visible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"BadRequest":{"description":"Requête invalide (validation ou règle métier)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Conflict":{"description":"Conflit d’état concurrent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"schemas":{"UserPublic":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"emailVerifiedAt":{"type":"string","format":"date-time","nullable":true},"phone":{"type":"string","nullable":true,"example":"+33123456789"},"phoneVerifiedAt":{"type":"string","format":"date-time","nullable":true},"role":{"type":"string","enum":["USER","ADVERTISER","ADMIN"]},"emailNotificationsEnabled":{"type":"boolean"},"smsNotificationsEnabled":{"type":"boolean"},"listingContactPhone":{"type":"string","nullable":true,"description":"Téléphone affiché sur les annonces (annonceur)."},"listingContactWhatsapp":{"type":"string","nullable":true,"description":"WhatsApp affiché (E.164)."},"shareListingContact":{"type":"boolean","description":"Publier le contact sur les fiches annonce."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","email","role","emailNotificationsEnabled","smsNotificationsEnabled","shareListingContact","createdAt","updatedAt"]},"ListingSellerContact":{"type":"object","properties":{"phone":{"type":"string","nullable":true},"whatsapp":{"type":"string","nullable":true},"whatsappLink":{"type":"string","nullable":true,"description":"Lien wa.me prêt à l’emploi."}}},"AuthTokensResponse":{"type":"object","properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"tokenType":{"type":"string","enum":["Bearer"]},"expiresIn":{"type":"integer","description":"Durée de vie du jeton d’accès en secondes"},"user":{"$ref":"#/components/schemas/UserPublic"}},"required":["accessToken","refreshToken","tokenType","expiresIn","user"]},"RegisterRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8},"phone":{"type":"string","example":"+33123456789"}},"required":["email","password"]},"RequestPasswordReset":{"type":"object","properties":{"email":{"type":"string","format":"email"}},"required":["email"]},"ConfirmPasswordReset":{"type":"object","properties":{"email":{"type":"string","format":"email"},"code":{"type":"string","pattern":"^\\d{6}$"},"newPassword":{"type":"string","minLength":8,"maxLength":128}},"required":["email","code","newPassword"]},"VerifyAccountCode":{"type":"object","properties":{"code":{"type":"string","pattern":"^\\d{6}$"}},"required":["code"]},"LoginRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"}},"required":["email","password"]},"RefreshTokenRequest":{"type":"object","properties":{"refreshToken":{"type":"string"}},"required":["refreshToken"]},"UpdateProfileRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"},"phone":{"type":"string","nullable":true},"listingContactPhone":{"type":"string","nullable":true},"listingContactWhatsapp":{"type":"string","nullable":true},"shareListingContact":{"type":"boolean"}}},"NotificationPreferences":{"type":"object","properties":{"emailNotificationsEnabled":{"type":"boolean"},"smsNotificationsEnabled":{"type":"boolean"}},"required":["emailNotificationsEnabled","smsNotificationsEnabled"]},"UpdateNotificationPreferencesRequest":{"type":"object","properties":{"emailNotificationsEnabled":{"type":"boolean"},"smsNotificationsEnabled":{"type":"boolean"}}},"ErrorResponse":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"error":{"type":"string"}},"required":["statusCode","message"]},"HealthCheck":{"type":"object","properties":{"id":{"type":"string","example":"postgres"},"name":{"type":"string","example":"PostgreSQL"},"configured":{"type":"boolean"},"functional":{"type":"boolean"},"required":{"type":"boolean"},"detail":{"type":"string"}},"required":["id","name","configured","functional","required","detail"]},"HealthResponse":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded","down"],"example":"ok"},"service":{"type":"string","example":"odealz_backend"},"timestamp":{"type":"string","format":"date-time"},"checks":{"type":"array","items":{"$ref":"#/components/schemas/HealthCheck"}}},"required":["status","service","timestamp","checks"]},"ListingPrice":{"type":"object","properties":{"amountCents":{"type":"integer","minimum":1},"currency":{"type":"string","example":"EUR"}},"required":["amountCents","currency"]},"ListingCategory":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"parentSlug":{"type":"string","nullable":true}},"required":["slug","name","parentSlug"]},"CategoryTreeNode":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"commercialOnly":{"type":"boolean"},"children":{"type":"array","items":{"$ref":"#/components/schemas/CategoryTreeNode"}}},"required":["slug","name","commercialOnly","children"]},"Listing":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"category":{"nullable":true,"description":"Catégorie sélectionnée ; null pour les annonces historiques antérieures à la catégorisation.","allOf":[{"$ref":"#/components/schemas/ListingCategory"}]},"images":{"type":"array","items":{"$ref":"#/components/schemas/ListingImage"}},"type":{"type":"string","enum":["COMMERCIAL","NON_COMMERCIAL"]},"condition":{"type":"string","nullable":true,"enum":["NEW","LIKE_NEW","GOOD","FAIR","FOR_PARTS"],"description":"État facultatif de l’objet ; omis pour les services si sans objet."},"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string","enum":["DRAFT","PUBLISHED","RESERVED","COMPLETED","ARCHIVED","EXPIRED","REMOVED"]},"pickupLocationLabel":{"type":"string","nullable":true},"pickupAddress":{"type":"string","nullable":true,"description":"Adresse exacte privée : toujours null dans le catalogue public, fournie uniquement dans les réponses destinées au propriétaire."},"pickupCity":{"type":"string","nullable":true},"pickupUnit":{"type":"string","nullable":true,"description":"Unité/appartement ; privé."},"pickupStreetAddress":{"type":"string","nullable":true,"description":"Numéro et rue ; privé."},"pickupPostalCode":{"type":"string","nullable":true,"description":"Code postal canadien, privé."},"pickupProvince":{"type":"string","nullable":true,"enum":["AB","BC","MB","NB","NL","NS","NT","NU","ON","PE","QC","SK","YT"]},"pickupCountry":{"type":"string","nullable":true},"distanceKm":{"type":"number","nullable":true,"description":"Distance arrondie depuis le centre de recherche ; uniquement pour une recherche par rayon."},"publishedAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"price":{"$ref":"#/components/schemas/ListingPrice"},"sellerContact":{"nullable":true,"description":"Contact annonceur sur la fiche publique (`GET /listings/{id}`) si partage activé.","allOf":[{"$ref":"#/components/schemas/ListingSellerContact"}]}}},"ListingImage":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string"},"mimeType":{"type":"string","enum":["image/jpeg","image/png","image/webp"]},"sizeBytes":{"type":"integer","maximum":8388608},"sortOrder":{"type":"integer","minimum":0}}},"PaginatedListings":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Listing"}},"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"}}},"CreateListingRequest":{"type":"object","properties":{"type":{"type":"string","enum":["COMMERCIAL","NON_COMMERCIAL"]},"categorySlug":{"type":"string","description":"Slug d’une sous-catégorie active."},"condition":{"type":"string","enum":["NEW","LIKE_NEW","GOOD","FAIR","FOR_PARTS"]},"title":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":10000},"pickupLocationLabel":{"type":"string"},"pickupAddress":{"type":"string"},"pickupUnit":{"type":"string","maxLength":40},"pickupStreetAddress":{"type":"string","maxLength":255},"pickupCity":{"type":"string"},"pickupPostalCode":{"type":"string","pattern":"^[ABCEGHJ-NPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d$"},"pickupProvince":{"type":"string","enum":["AB","BC","MB","NB","NL","NS","NT","NU","ON","PE","QC","SK","YT"]},"pickupCountry":{"type":"string","enum":["CA"],"default":"CA"},"pickupLatitude":{"type":"number","minimum":41,"maximum":84,"description":"Privé ; doit être fourni avec pickupLongitude."},"pickupLongitude":{"type":"number","minimum":-141,"maximum":-52,"description":"Privé ; doit être fourni avec pickupLatitude."},"priceAmountCents":{"type":"integer","minimum":1},"currency":{"type":"string","example":"EUR"}},"required":["type","categorySlug","title","description","pickupLocationLabel"]},"UpdateListingRequest":{"type":"object","properties":{"condition":{"type":"string","enum":["NEW","LIKE_NEW","GOOD","FAIR","FOR_PARTS"]},"categorySlug":{"type":"string","description":"Slug d’une sous-catégorie active."},"title":{"type":"string"},"description":{"type":"string"},"pickupLocationLabel":{"type":"string"},"pickupAddress":{"type":"string"},"pickupUnit":{"type":"string","maxLength":40},"pickupStreetAddress":{"type":"string","maxLength":255},"pickupCity":{"type":"string"},"pickupPostalCode":{"type":"string","pattern":"^[ABCEGHJ-NPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d$"},"pickupProvince":{"type":"string","enum":["AB","BC","MB","NB","NL","NS","NT","NU","ON","PE","QC","SK","YT"]},"pickupCountry":{"type":"string","enum":["CA"]},"pickupLatitude":{"type":"number","minimum":41,"maximum":84},"pickupLongitude":{"type":"number","minimum":-141,"maximum":-52},"priceAmountCents":{"type":"integer","minimum":1},"currency":{"type":"string"}}},"ChangeListingPriceRequest":{"type":"object","properties":{"amountCents":{"type":"integer","minimum":1},"currency":{"type":"string","example":"EUR"}},"required":["amountCents","currency"]},"ListingPriceHistoryItem":{"type":"object","properties":{"amountCents":{"type":"integer"},"currency":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"PaginatedListingPriceHistory":{"type":"object","properties":{"listingId":{"type":"string","format":"uuid"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ListingPriceHistoryItem"}},"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"}}},"FavoriteListingSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["COMMERCIAL","NON_COMMERCIAL"]},"title":{"type":"string"},"status":{"type":"string","enum":["DRAFT","PUBLISHED","RESERVED","COMPLETED","ARCHIVED"]},"price":{"$ref":"#/components/schemas/ListingPrice"}},"required":["id","type","title","status"]},"Favorite":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"listingId":{"type":"string","format":"uuid"},"notifyPriceChange":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"listing":{"$ref":"#/components/schemas/FavoriteListingSummary"}},"required":["id","listingId","notifyPriceChange","createdAt","listing"]},"PaginatedFavorites":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Favorite"}},"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"}}},"NotificationPublicPayload":{"type":"object","description":"Contexte métier affichable (sans jetons ni données de contact).","properties":{"listingId":{"type":"string","format":"uuid"},"conversationId":{"type":"string","format":"uuid"},"messageId":{"type":"string","format":"uuid"},"pickupId":{"type":"string","format":"uuid"},"pickupStatus":{"type":"string"},"pickupEvent":{"type":"string","description":"Événement métier de remise (ex. confirmed, rejected) lorsque le statut seul est ambigu."},"listingPriceId":{"type":"string","format":"uuid"},"amountCents":{"type":"integer"},"currency":{"type":"string","example":"EUR"}}},"NotificationItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"channel":{"type":"string","enum":["EMAIL","SMS"]},"type":{"type":"string","enum":["NEW_MESSAGE","PICKUP_PROPOSED","PICKUP_UPDATED","PRICE_CHANGED","LISTING_STATUS_CHANGED","PASSWORD_RESET","EMAIL_VERIFICATION","PHONE_VERIFICATION","SAVED_SEARCH_MATCH"]},"status":{"type":"string","enum":["PENDING","SENT","FAILED"]},"payload":{"$ref":"#/components/schemas/NotificationPublicPayload"},"createdAt":{"type":"string","format":"date-time"}},"required":["id","channel","type","status","payload","createdAt"]},"PaginatedNotifications":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/NotificationItem"}},"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"}},"required":["items","page","limit","total","totalPages"]},"SavedSearchWriteRequest":{"type":"object","properties":{"name":{"type":"string","maxLength":100},"type":{"type":"string","enum":["COMMERCIAL","NON_COMMERCIAL"],"nullable":true},"categorySlug":{"type":"string","nullable":true,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"city":{"type":"string","nullable":true,"maxLength":120},"province":{"type":"string","nullable":true,"enum":["AB","BC","MB","NB","NL","NS","NT","NU","ON","PE","QC","SK","YT"]},"country":{"type":"string","nullable":true,"pattern":"^[A-Za-z]{2}$"},"minPriceCents":{"type":"integer","nullable":true,"minimum":1},"maxPriceCents":{"type":"integer","nullable":true,"minimum":1},"currency":{"type":"string","nullable":true,"pattern":"^[A-Za-z]{3}$"},"latitude":{"type":"number","nullable":true,"minimum":41,"maximum":84},"longitude":{"type":"number","nullable":true,"minimum":-141,"maximum":-52},"radiusKm":{"type":"integer","nullable":true,"minimum":1,"maximum":100},"isActive":{"type":"boolean"}}},"SavedSearch":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"userId":{"type":"string","format":"uuid"},"name":{"type":"string"},"type":{"type":"string","enum":["COMMERCIAL","NON_COMMERCIAL"],"nullable":true},"categorySlug":{"type":"string","nullable":true},"category":{"type":"object","nullable":true,"properties":{"slug":{"type":"string"},"name":{"type":"string"}}},"city":{"type":"string","nullable":true},"province":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"latitude":{"type":"number","nullable":true},"longitude":{"type":"number","nullable":true},"radiusKm":{"type":"integer","nullable":true},"minPriceCents":{"type":"integer","nullable":true},"maxPriceCents":{"type":"integer","nullable":true},"currency":{"type":"string","nullable":true},"isActive":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PaginatedSavedSearches":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SavedSearch"}},"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"}},"required":["items","page","limit","total","totalPages"]},"UpsertFavoriteRequest":{"type":"object","properties":{"notifyPriceChange":{"type":"boolean"}}},"UpdateFavoriteRequest":{"type":"object","properties":{"notifyPriceChange":{"type":"boolean"}},"required":["notifyPriceChange"]},"ConversationListingSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"type":{"type":"string","enum":["COMMERCIAL","NON_COMMERCIAL"]},"status":{"type":"string","enum":["DRAFT","PUBLISHED","RESERVED","COMPLETED","ARCHIVED"]}},"required":["id","title","type","status"]},"ConversationParticipant":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"}},"required":["id","email"]},"ConversationDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"listingId":{"type":"string","format":"uuid"},"interestedUserId":{"type":"string","format":"uuid"},"listing":{"$ref":"#/components/schemas/ConversationListingSummary"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","listingId","interestedUserId","listing","createdAt","updatedAt"]},"ConversationSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"listingId":{"type":"string","format":"uuid"},"interestedUserId":{"type":"string","format":"uuid"},"listing":{"$ref":"#/components/schemas/ConversationListingSummary"},"otherParticipant":{"$ref":"#/components/schemas/ConversationParticipant"},"lastMessage":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/Message"}]},"unreadCount":{"type":"integer","minimum":0,"description":"Messages reçus non lus depuis la dernière position de lecture."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","listingId","interestedUserId","listing","otherParticipant","unreadCount","createdAt","updatedAt"]},"ChatMessagesPage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"limit":{"type":"integer"},"hasMoreBefore":{"type":"boolean","description":"Messages plus anciens disponibles (scroll vers le haut)."},"hasMoreAfter":{"type":"boolean","description":"Messages plus récents disponibles (polling)."},"oldestMessageId":{"type":"string","format":"uuid","nullable":true},"newestMessageId":{"type":"string","format":"uuid","nullable":true}},"required":["items","limit","hasMoreBefore","hasMoreAfter","oldestMessageId","newestMessageId"]},"MarkConversationReadRequest":{"type":"object","properties":{"messageId":{"type":"string","format":"uuid","description":"Dernier message lu ; défaut = le plus récent."}}},"MarkConversationReadResponse":{"type":"object","properties":{"conversationId":{"type":"string","format":"uuid"},"lastReadAt":{"type":"string","format":"date-time"},"lastReadMessageId":{"type":"string","format":"uuid","nullable":true},"unreadCount":{"type":"integer","minimum":0}},"required":["conversationId","lastReadAt","lastReadMessageId","unreadCount"]},"PaginatedConversations":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ConversationSummary"}},"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"}},"required":["items","page","limit","total","totalPages"]},"PaginatedMessages":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"}},"required":["items","page","limit","total","totalPages"]},"Message":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"senderId":{"type":"string","format":"uuid"},"body":{"type":"string","maxLength":5000},"createdAt":{"type":"string","format":"date-time"}},"required":["id","senderId","body","createdAt"]},"Pickup":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"conversationId":{"type":"string","format":"uuid"},"proposedById":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PROPOSED","CONFIRMED","COMPLETED","CANCELLED"]},"scheduledAt":{"type":"string","format":"date-time","nullable":true},"locationLabel":{"type":"string","nullable":true},"locationAddress":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","conversationId","proposedById","status","createdAt","updatedAt"]},"SendMessageRequest":{"type":"object","properties":{"body":{"type":"string","maxLength":5000}},"required":["body"]},"ProposePickupRequest":{"type":"object","properties":{"scheduledAt":{"type":"string","format":"date-time"},"locationLabel":{"type":"string"},"locationAddress":{"type":"string"}},"required":["scheduledAt","locationLabel"]},"UpdatePickupRequest":{"type":"object","properties":{"action":{"type":"string","enum":["confirm","reject","cancel","complete"]}},"required":["action"]},"CreateReportRequest":{"type":"object","properties":{"reason":{"type":"string","enum":["SPAM","FRAUD","PROHIBITED_ITEM","HARASSMENT","INAPPROPRIATE_CONTENT","OTHER"]},"details":{"type":"string","maxLength":2000}},"required":["reason"]},"ReviewReportRequest":{"type":"object","properties":{"action":{"type":"string","enum":["DISMISS","HIDE_LISTING","HIDE_IMAGE","SUSPEND_USER"]},"resolution":{"type":"string","maxLength":1000}},"required":["action"]},"CreatePickupReviewRequest":{"type":"object","properties":{"rating":{"type":"integer","minimum":1,"maximum":5}},"required":["rating"]},"PickupReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"pickupId":{"type":"string","format":"uuid"},"rating":{"type":"integer","minimum":1,"maximum":5},"createdAt":{"type":"string","format":"date-time"}},"required":["id","pickupId","rating","createdAt"]},"PublicReputation":{"type":"object","properties":{"userId":{"type":"string","format":"uuid"},"averageRating":{"type":"number","nullable":true,"minimum":1,"maximum":5},"reviewCount":{"type":"integer","minimum":0},"distribution":{"type":"array","items":{"type":"object","properties":{"rating":{"type":"integer","minimum":1,"maximum":5},"count":{"type":"integer","minimum":0}},"required":["rating","count"]}}},"required":["userId","averageRating","reviewCount","distribution"]},"CategoryWriteRequest":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"name":{"type":"string","maxLength":120},"description":{"type":"string","maxLength":500},"parentSlug":{"type":"string"},"sortOrder":{"type":"integer","minimum":0},"commercialOnly":{"type":"boolean"}},"required":["slug","name"]},"CategoryUpdateRequest":{"type":"object","properties":{"name":{"type":"string","maxLength":120},"description":{"type":"string","nullable":true,"maxLength":500},"parentSlug":{"type":"string","nullable":true},"sortOrder":{"type":"integer","minimum":0},"commercialOnly":{"type":"boolean"},"isActive":{"type":"boolean"}}}}},"paths":{"/categories":{"get":{"tags":["Catégories"],"summary":"Lister l’arbre des catégories actives","description":"Retourne les catégories racines et leurs sous-catégories. Les catégories réservées aux annonces commerciales sont masquées si le mode commercial est désactivé ou si listingType=NON_COMMERCIAL.","parameters":[{"name":"listingType","in":"query","schema":{"type":"string","enum":["COMMERCIAL","NON_COMMERCIAL"]}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CategoryTreeNode"}}}}}}}},"/admin/categories":{"post":{"tags":["Catégories"],"summary":"Créer une catégorie (administration)","description":"Réservé au rôle ADMIN. Le slug est stable et unique ; la catégorie est active à sa création.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategoryWriteRequest"}}}},"responses":{"201":{"description":"Catégorie créée"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/admin/categories/{slug}":{"patch":{"tags":["Catégories"],"summary":"Modifier une catégorie (administration)","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategoryUpdateRequest"}}}},"responses":{"200":{"description":"Catégorie modifiée"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Catégories"],"summary":"Désactiver une catégorie et ses descendantes","description":"Désactivation logique ; les annonces historiques conservent leur relation.","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Nombre de catégories désactivées"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/listings/{id}/renew":{"post":{"tags":["Annonces"],"summary":"Renouveler une annonce","security":[{"bearerAuth":[]}],"description":"Propriétaire annonceur uniquement. Prolonge la publication de 30 jours. Refus si réservée, terminée, archivée ou retirée.","parameters":[{"$ref":"#/components/parameters/ListingIdPath"}],"responses":{"200":{"description":"Annonce renouvelée."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/listings/{id}/images":{"post":{"tags":["Annonces"],"summary":"Téléverser une image","security":[{"bearerAuth":[]}],"description":"Annonceur propriétaire uniquement. JPEG, PNG ou WebP ; 8 Mo maximum et 10 images par annonce. Le fichier est réencodé et les métadonnées sont retirées avant stockage.","parameters":[{"$ref":"#/components/parameters/ListingIdPath"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]}}}},"responses":{"201":{"description":"Image créée."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"description":"Limite de téléversement atteinte."}}}},"/listings/{id}/images/{imageId}":{"delete":{"tags":["Annonces"],"summary":"Supprimer une image","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/ListingIdPath"},{"name":"imageId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Image supprimée."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/listings/{id}/images/order":{"patch":{"tags":["Annonces"],"summary":"Réordonner les images","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/ListingIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"imageIds":{"type":"array","maxItems":10,"uniqueItems":true,"items":{"type":"string","format":"uuid"}}},"required":["imageIds"]}}}},"responses":{"200":{"description":"Ordre des images mis à jour."},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/admin/moderation/images/{imageId}":{"get":{"tags":["Confiance et sécurité"],"summary":"Consulter une photo signalée (ADMIN)","security":[{"bearerAuth":[]}],"parameters":[{"name":"imageId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Image binaire, privée et non mise en cache."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/media/{imageId}":{"get":{"tags":["Annonces"],"summary":"Récupérer une image publiquement visible","servers":[{"url":"/"}],"description":"Accessible seulement pour une annonce publiée, non expirée et visible selon le mode commercial.","parameters":[{"name":"imageId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Image binaire."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/listings/{id}/reports":{"post":{"tags":["Confiance et sécurité"],"summary":"Signaler une annonce","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/ListingIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportRequest"}}}},"responses":{"201":{"description":"Signalement enregistré."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"description":"Limite de signalements atteinte."}}}},"/listings/{id}/images/{imageId}/reports":{"post":{"tags":["Confiance et sécurité"],"summary":"Signaler une photo d’annonce","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/ListingIdPath"},{"name":"imageId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportRequest"}}}},"responses":{"201":{"description":"Signalement placé dans la file de modération."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/users/{userId}/reports":{"post":{"tags":["Confiance et sécurité"],"summary":"Signaler un compte","security":[{"bearerAuth":[]}],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReportRequest"}}}},"responses":{"201":{"description":"Signalement enregistré."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/users/{userId}/reputation":{"get":{"tags":["Confiance et sécurité"],"summary":"Consulter la réputation publique d’un annonceur","description":"Expose uniquement les agrégats des notes reçues après des remises terminées. Les comptes suspendus ou non annonceurs ne sont pas trouvés.","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicReputation"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/me/blocked-users/{userId}":{"post":{"tags":["Confiance et sécurité"],"summary":"Bloquer un compte","security":[{"bearerAuth":[]}],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"201":{"description":"Compte bloqué."},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Confiance et sécurité"],"summary":"Débloquer un compte","security":[{"bearerAuth":[]}],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Blocage retiré ou déjà absent."}}}},"/me/blocked-users":{"get":{"tags":["Confiance et sécurité"],"summary":"Lister les comptes bloqués","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Identifiants et dates de blocage."},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/admin/reports":{"get":{"tags":["Confiance et sécurité"],"summary":"Lister les signalements ouverts (ADMIN)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/PageQuery"},{"$ref":"#/components/parameters/LimitQuery"}],"responses":{"200":{"description":"Signalements ouverts."},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/admin/moderation":{"get":{"tags":["Confiance et sécurité"],"summary":"File paginée des signalements à examiner (ADMIN)","description":"Comprend les signalements d’annonces, de photos et de comptes.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/PageQuery"},{"$ref":"#/components/parameters/LimitQuery"}],"responses":{"200":{"description":"Éléments en attente de revue."},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/admin/audit-logs":{"get":{"tags":["Confiance et sécurité"],"summary":"Consulter le journal des actions d’administration (ADMIN)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/PageQuery"},{"$ref":"#/components/parameters/LimitQuery"},{"name":"targetType","in":"query","schema":{"type":"string"}},{"name":"targetId","in":"query","schema":{"type":"string"}},{"name":"actorId","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Événements d’audit paginés."},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/admin/reports/{id}":{"patch":{"tags":["Confiance et sécurité"],"summary":"Traiter un signalement (ADMIN)","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewReportRequest"}}}},"responses":{"200":{"description":"Signalement traité."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/health":{"get":{"tags":["Système"],"summary":"Santé du service","description":"Hors préfixe `/api/v1`. Public, sans JWT. `ok` si tous les contrôles passent, `degraded` si un service facultatif est mal configuré ou injoignable, `down` si un service obligatoire ne répond pas. Le navigateur reçoit du HTML ; `?format=json` force le JSON.","servers":[{"url":"/"}],"responses":{"200":{"description":"Service opérationnel ou dégradé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}},"text/html":{"schema":{"type":"string"}}}},"503":{"description":"Un service obligatoire est injoignable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/auth/register":{"post":{"tags":["Auth"],"summary":"Inscription","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}}},"responses":{"201":{"description":"Compte créé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokensResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"description":"E-mail déjà utilisé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/login":{"post":{"tags":["Auth"],"summary":"Connexion","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}}},"responses":{"200":{"description":"Jetons émis","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokensResponse"}}}},"401":{"description":"Identifiants invalides","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/password-reset/request":{"post":{"tags":["Auth"],"summary":"Demander un code de réinitialisation","description":"Réponse identique que le compte existe ou non. Limité à 3 demandes par minute et par client.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestPasswordReset"}}}},"responses":{"202":{"description":"Demande acceptée sans révéler si l’adresse est enregistrée"},"400":{"$ref":"#/components/responses/BadRequest"},"503":{"description":"Clé de chiffrement de code non configurée"}}}},"/auth/password-reset/confirm":{"post":{"tags":["Auth"],"summary":"Confirmer le code et choisir un nouveau mot de passe","description":"Code à usage unique, valable 15 minutes. Les jetons de rafraîchissement sont révoqués après confirmation.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmPasswordReset"}}}},"responses":{"200":{"description":"Mot de passe modifié"},"400":{"description":"Code invalide, expiré ou déjà utilisé"}}}},"/auth/refresh":{"post":{"tags":["Auth"],"summary":"Rafraîchir les jetons","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}}},"responses":{"200":{"description":"Nouveaux jetons (rotation du refresh)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokensResponse"}}}},"401":{"description":"Jeton invalide ou révoqué"}}}},"/auth/logout":{"post":{"tags":["Auth"],"summary":"Déconnexion (révocation du refresh token)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}}},"responses":{"204":{"description":"Jeton révoqué ou déjà invalide"}}}},"/me/email-verification/request":{"post":{"tags":["Profil"],"summary":"Envoyer un code de vérification e-mail","description":"JWT requis. Envoi transactionnel, sans dépendre des préférences de notifications. Le code expire après 15 minutes.","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Code mis en file d’envoi ou adresse déjà vérifiée"},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"description":"Clé de chiffrement non configurée"}}}},"/me/email-verification/confirm":{"post":{"tags":["Profil"],"summary":"Confirmer le code e-mail","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyAccountCode"}}}},"responses":{"201":{"description":"Adresse e-mail vérifiée"},"400":{"description":"Code invalide, expiré ou déjà utilisé"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/me/phone-verification/request":{"post":{"tags":["Profil"],"summary":"Envoyer un code de vérification SMS","description":"JWT requis. Le numéro doit être renseigné. Envoi transactionnel via Twilio, sans dépendre des préférences SMS.","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Code mis en file d’envoi ou téléphone déjà vérifié"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"description":"Clé de chiffrement non configurée"}}}},"/me/phone-verification/confirm":{"post":{"tags":["Profil"],"summary":"Confirmer le code SMS","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyAccountCode"}}}},"responses":{"201":{"description":"Téléphone vérifié"},"400":{"description":"Code invalide, expiré ou déjà utilisé"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/me":{"get":{"tags":["Profil"],"summary":"Profil courant","security":[{"bearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPublic"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"patch":{"tags":["Profil"],"summary":"Mettre à jour le profil","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileRequest"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPublic"}}}}}},"delete":{"tags":["Profil"],"summary":"Supprimer définitivement son compte","security":[{"bearerAuth":[]}],"description":"Confirmation du mot de passe requise. Les données liées sont supprimées selon les relations de cascade.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"password":{"type":"string","minLength":1}},"required":["password"]}}}},"responses":{"204":{"description":"Compte supprimé."},"403":{"description":"Mot de passe incorrect."}}}},"/me/export":{"get":{"tags":["Profil"],"summary":"Exporter ses données personnelles (JSON)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Profil, annonces, favoris, conversations et historique des notifications."},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/me/become-advertiser":{"post":{"tags":["Profil"],"summary":"Passer au rôle annonceur","security":[{"bearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPublic"}}}}}}},"/me/notification-preferences":{"get":{"tags":["Profil"],"summary":"Préférences de notification","security":[{"bearerAuth":[]}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationPreferences"}}}}}},"patch":{"tags":["Profil"],"summary":"Modifier les préférences de notification","security":[{"bearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNotificationPreferencesRequest"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationPreferences"}}}}}}},"/listings":{"get":{"tags":["Annonces"],"summary":"Catalogue public paginé","description":"Uniquement les annonces **PUBLISHED**. Si `status` est fourni et différent de `PUBLISHED`, réponse **400**. Sans mode commercial, seules les annonces **NON_COMMERCIAL** sont listées.","parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["COMMERCIAL","NON_COMMERCIAL"]},"description":"Filtre (annonces commerciales absentes si mode commercial désactivé)."},{"name":"status","in":"query","schema":{"type":"string","enum":["PUBLISHED"],"default":"PUBLISHED"}},{"name":"categorySlug","in":"query","schema":{"type":"string"},"description":"Slug de catégorie ; inclut les annonces des sous-catégories."},{"name":"q","in":"query","schema":{"type":"string","maxLength":120},"description":"Recherche dans le titre et la description."},{"name":"city","in":"query","schema":{"type":"string","maxLength":120}},{"name":"province","in":"query","schema":{"type":"string","enum":["AB","BC","MB","NB","NL","NS","NT","NU","ON","PE","QC","SK","YT"]}},{"name":"latitude","in":"query","schema":{"type":"number","minimum":41,"maximum":84}},{"name":"longitude","in":"query","schema":{"type":"number","minimum":-141,"maximum":-52}},{"name":"radiusKm","in":"query","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"country","in":"query","schema":{"type":"string","pattern":"^[A-Z]{2}$"}},{"name":"minPriceCents","in":"query","schema":{"type":"integer","minimum":1}},{"name":"maxPriceCents","in":"query","schema":{"type":"integer","minimum":1}},{"name":"sort","in":"query","schema":{"type":"string","enum":["NEWEST","PRICE_ASC","PRICE_DESC","NEAREST"]}},{"$ref":"#/components/parameters/PageQuery"},{"$ref":"#/components/parameters/LimitQuery"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedListings"}}}},"400":{"$ref":"#/components/responses/BadRequest"}}},"post":{"tags":["Annonces"],"summary":"Créer une annonce (annonceur)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateListingRequest"}}}},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}},"403":{"description":"Non annonceur ou annonces commerciales désactivées"}}}},"/listings/{id}/prices":{"get":{"tags":["Annonces"],"summary":"Historique des prix (annonce commerciale)","description":"JWT **optionnel**. Propriétaire : accès même si mode commercial désactivé. Autres utilisateurs : mode commercial requis et annonce **PUBLISHED**.","security":[{"bearerAuth":[]},{}],"parameters":[{"$ref":"#/components/parameters/ListingIdPath"},{"$ref":"#/components/parameters/PageQuery"},{"$ref":"#/components/parameters/LimitQuery"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedListingPriceHistory"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/listings/{id}/price":{"post":{"tags":["Annonces"],"summary":"Modifier le prix (propriétaire, annonce commerciale)","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeListingPriceRequest"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}},"403":{"description":"Mode commercial désactivé ou non propriétaire"}}}},"/listings/{id}":{"get":{"tags":["Annonces"],"summary":"Détail public d’une annonce publiée","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}},"404":{"description":"Annonce introuvable ou non visible"}}},"patch":{"tags":["Annonces"],"summary":"Modifier une annonce (propriétaire annonceur)","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateListingRequest"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}},"403":{"description":"Non propriétaire ou mode commercial désactivé"},"404":{"description":"Annonce introuvable"}}}},"/listings/{id}/archive":{"post":{"tags":["Annonces"],"summary":"Archiver une annonce","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}}}}},"/me/listings":{"get":{"tags":["Annonces"],"summary":"Mes annonces (tous statuts, y compris commerciales masquées du public)","security":[{"bearerAuth":[]}],"parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["COMMERCIAL","NON_COMMERCIAL"]}},{"name":"status","in":"query","schema":{"type":"string","enum":["DRAFT","PUBLISHED","RESERVED","COMPLETED","ARCHIVED"]}},{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedListings"}}}}}}},"/me/saved-searches":{"get":{"tags":["Recherches enregistrées"],"summary":"Lister mes recherches enregistrées","description":"JWT requis. Pagination par page et limite (1 à 100). Maximum 20 recherches par compte.","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedSavedSearches"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Recherches enregistrées"],"summary":"Enregistrer une recherche et ses alertes","description":"Les recherches par prix requièrent une devise et concernent les annonces commerciales. Une catégorie parente inclut ses sous-catégories actives. Max. 20 recherches par compte.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchWriteRequest"}}}},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearch"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Recherche commerciale désactivée."},"409":{"$ref":"#/components/responses/Conflict"},"429":{"description":"Limite de création atteinte."}}}},"/me/saved-searches/{id}":{"patch":{"tags":["Recherches enregistrées"],"summary":"Modifier ou suspendre une recherche enregistrée","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchWriteRequest"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearch"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Recherche commerciale désactivée."},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Recherches enregistrées"],"summary":"Supprimer une recherche enregistrée","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Recherche supprimée."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/me/notifications":{"get":{"tags":["Notifications"],"summary":"Mes notifications (historique)","description":"Propriétaire des lignes uniquement. Statuts **PENDING** (en cours d’envoi), **SENT**, **FAILED**. Pas de contenu privé des messages dans le payload.","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedNotifications"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/me/favorites":{"get":{"tags":["Favoris"],"summary":"Mes favoris","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedFavorites"}}}}}}},"/me/favorites/{listingId}":{"put":{"tags":["Favoris"],"summary":"Ajouter ou mettre à jour un favori (idempotent)","security":[{"bearerAuth":[]}],"parameters":[{"name":"listingId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertFavoriteRequest"}}}},"responses":{"200":{"description":"Favori créé ou déjà existant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Favorite"}}}}}},"patch":{"tags":["Favoris"],"summary":"Modifier l’alerte de changement de prix","security":[{"bearerAuth":[]}],"parameters":[{"name":"listingId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFavoriteRequest"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Favorite"}}}},"400":{"description":"Alerte non autorisée pour ce type d’annonce"}}},"delete":{"tags":["Favoris"],"summary":"Retirer un favori","security":[{"bearerAuth":[]}],"parameters":[{"name":"listingId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Supprimé ou déjà absent"}}}},"/listings/{id}/conversations":{"post":{"tags":["Conversations"],"summary":"Démarrer ou retrouver une conversation","description":"Idempotent : retourne la conversation existante pour ce couple (annonce, utilisateur intéressé). Refus si propriétaire de l’annonce, annonce non **PUBLISHED**, ou nouvelle conversation commerciale avec mode commercial désactivé.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/ListingIdPath"}],"responses":{"200":{"description":"Conversation existante ou créée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetail"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/me/conversations":{"get":{"tags":["Conversations"],"summary":"Mes conversations (propriétaire ou intéressé)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/PageQuery"},{"$ref":"#/components/parameters/LimitQuery"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedConversations"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/conversations/{id}/messages":{"get":{"tags":["Conversations"],"summary":"Messages d’une conversation (chat)","description":"Participant uniquement. Sans `page` : fenêtre chat (derniers messages, curseurs `before` / `after`). Avec `page` : pagination classique (`PaginatedMessages`).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/ConversationIdPath"},{"$ref":"#/components/parameters/PageQuery"},{"$ref":"#/components/parameters/LimitQuery"},{"name":"before","in":"query","schema":{"type":"string","format":"uuid"},"description":"Messages plus anciens que ce message."},{"name":"after","in":"query","schema":{"type":"string","format":"uuid"},"description":"Messages plus récents que ce message (polling)."}],"responses":{"200":{"description":"`ChatMessagesPage` sans `page`, sinon `PaginatedMessages`.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ChatMessagesPage"},{"$ref":"#/components/schemas/PaginatedMessages"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Conversations"],"summary":"Envoyer un message","description":"Participant uniquement. Refus si annonce **ARCHIVED** ou **COMPLETED**. Déclenche une notification au autre participant (asynchrone).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/ConversationIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageRequest"}}}},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/conversations/{id}/read":{"post":{"tags":["Conversations"],"summary":"Marquer la conversation comme lue","description":"Met à jour le curseur de lecture du participant connecté. Corps optionnel (`messageId`).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/ConversationIdPath"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkConversationReadRequest"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkConversationReadResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/conversations/{id}/pickups":{"post":{"tags":["Conversations"],"summary":"Proposer une remise","description":"Participant uniquement. Lieu et horaire visibles des participants. Une seule proposition **PROPOSED** ou **CONFIRMED** à la fois.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/ConversationIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposePickupRequest"}}}},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pickup"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/pickups/{id}":{"patch":{"tags":["Conversations"],"summary":"Transition de remise","description":"Voir la section « Remises » de la description générale. Participant ou destinataire selon l’action.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePickupRequest"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pickup"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/pickups/{id}/review":{"post":{"tags":["Confiance et sécurité"],"summary":"Évaluer l’annonceur après une remise terminée","description":"JWT requis. Seul l’intéressé de la conversation peut noter l’annonceur, une fois par remise complétée. Aucun commentaire libre n’est accepté.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePickupReviewRequest"}}}},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PickupReview"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}}}}