# Vitis (cepage)

« Strava du vin » : chaque consommation (bouteille ou verre) est une activité
géolocalisée, notée et documentée, avec double localisation — origine du vin et
lieu de consommation — plus une cave à vin virtuelle.

## Stack

- **Backend** : Symfony 7.4 LTS + PHP 8.3 (parité avec l'hébergement mutualisé
  OVH cible), Doctrine ORM + MySQL 8.0, Auth par cookie de session
  (`json_login`), Symfony Validator.
- **Frontend** : SPA React (Vite + TypeScript + React Router + TanStack
  Query) dans `development/front/`, compilée vers `public/front/`.
- **Intégration** : un template Twig (`templates/front/index.html.twig`) sert
  la coquille SPA sur toutes les routes front ; React Router prend la main
  côté client. La page de partage `/a/{shareId}` reste rendue côté serveur
  (Twig) pour porter les balises Open Graph.
- **MapLibre GL** + tuiles OpenFreeMap (style monochrome, sans clé API).
- Photos : stockage disque local derrière une interface `StorageInterface`
  (`src/Service/Storage`) — brancher S3 en prod si besoin.
- **Docker Compose** : `mysql`, `php` (8.3-fpm), `caddy` (reverse proxy),
  `adminer`.

> Le projet a d'abord été prototypé en Next.js/PostgreSQL/PostGIS (voir
> l'historique git) avant ce portage vers Symfony + React SPA + MySQL,
> pour coller à un hébergement mutualisé OVH PHP/MySQL.

## Démarrage

```bash
docker compose up -d
docker compose exec php composer install
docker compose exec php bin/console doctrine:database:create --if-not-exists
docker compose exec php bin/console doctrine:migrations:migrate --no-interaction
docker compose exec php bin/console doctrine:fixtures:load --no-interaction

cd development/front
npm install
npm run dev          # serveur Vite (HMR), voir VITE_DEV_SERVER dans .env
```

App disponible sur **http://localhost:8090** (Caddy). Adminer sur
**http://localhost:8080** (serveur `database`, utilisateur/mot de passe `app`).

Comptes de démo : `alice@vitis.dev` / `bob@vitis.dev`, mot de passe `vitis1234`.

En prod (`APP_ENV=prod`), builder le front (`npm run build` dans
`development/front`, dépose dans `public/front/`) : la coquille Twig lit alors
`public/front/.vite/manifest.json` au lieu de pointer vers le serveur Vite.

## Architecture

```
src/
  Entity/            # 11 entités Doctrine (attributs PHP, IDs Ulid)
  Repository/         # repositories Doctrine
  Service/            # logique métier (Access, Follow, Notification, Activity, Wine, Cellar, Social, User)
  Controller/Api/     # contrôleurs API (mêmes chemins que l'ancienne version Next.js)
  Controller/         # FrontController (coquille SPA), ShareController (page de partage)
  Dto/                # DTOs de requête + validation (Symfony Validator)
  Http/               # presenters (entité -> JSON), exceptions API typées
  Twig/               # ViteExtension (pont dev-server / manifest de build)
  Security/           # handlers d'auth (succès/échec login, entry point API)
migrations/            # migrations Doctrine
templates/             # coquille SPA + page de partage
development/front/      # SPA React (Vite), voir son propre README implicite dans le code
public/front/           # build front compilé (généré, gitignored)
public/uploads/         # photos uploadées (gitignored)
docker/                 # Dockerfile PHP, Caddyfile
```

Règles clés (portées telles quelles depuis la version Next.js) :

- `Wine` est un référentiel **mutualisé** entre utilisateurs (dédupliqué par
  domaine + nom + millésime, insensible à la casse — collation MySQL par
  défaut).
- Une `Activity` référence un `Wine` mais pas forcément un `CellarItem`.
- Consommer une **bouteille** `MY_CELLAR` décrémente le stock en transaction
  (refus si stock insuffisant) ; un **verre** ne décrémente pas.
- **Visibilité de compte** : `PUBLIC` (défaut, suivi sans validation) /
  `FOLLOWERS` (demande à accepter) / `PRIVATE`. Règle centralisée dans
  `AccessService::canView()` ; contenu non autorisé → 404 (jamais 403), pour
  ne pas révéler son existence.
- **Abonnements** : demande → acceptation/refus → « suivre en retour »
  proposé (jamais automatique).
- **Notifications internes** : demande de suivi, acceptation, nouvel abonné,
  nouvelle activité d'un compte suivi (fan-out à l'écriture), like, commentaire.
- **Partage** : chaque activité a un lien secret `/a/<shareId>` (capability
  URL) accessible sans compte, quelle que soit la visibilité.
- Entités sociales futures (`Badge`, `UserBadge`) posées en base, sans UI.

### Écart technique vs la version PostGIS

MySQL/InnoDB exige que les colonnes `SPATIAL INDEX` soient `NOT NULL`,
incompatible avec des coordonnées optionnelles. Les lat/lng restent donc de
simples colonnes `DECIMAL(9,6)` nullable, sans colonne géométrique ni index
spatial — aucune requête de proximité n'est encore implémentée. Le jour où la
Vague 4 (« cavistes proches ») est construite, ajouter une colonne `POINT NOT
NULL` dédiée à cette fonctionnalité précise.

### Piège Doctrine à connaître

`QueryBuilder::setParameter()` avec une **entité complète** (`User`,
`Activity`) ne convertit pas fiablement son identifiant `ulid` dans une
clause DQL — contrairement à l'API Criteria de Doctrine (`findBy`/`find`/
`count`), qui gère cela correctement. Toujours préférer `findBy(['user' =>
$user])` à `->where('x.user = :user')->setParameter('user', $user)` ; si une
clause DQL est vraiment nécessaire, binder explicitement l'identifiant typé :
`->setParameter('userId', $user->getId(), 'ulid')`.

## Périmètre V1 + Vague 2 (social)

Auth 18+, profil, log d'activité complet, feed perso, détail avec double
carte, cave basique, visibilité de compte, demandes d'abonnement,
notifications, likes/commentaires, partage par lien.

Hors périmètre : OCR étiquette, achat/affiliation (placeholder), premium
payant.

---

L'abus d'alcool est dangereux pour la santé. À consommer avec modération.
