Files
geo/api/src/Config/AppConfig.php
Pierre 50f55d825d feat: Implémentation complète Stripe Connect V1 - Configuration des paiements pour amicales
Cette intégration permet aux amicales de configurer leurs comptes Stripe Express
pour accepter les paiements par carte bancaire avec 0% de commission plateforme.

## 🎯 Fonctionnalités implémentées

### API PHP (Backend)
- **POST /api/stripe/accounts**: Création comptes Stripe Express
- **GET /api/stripe/accounts/:id/status**: Vérification statut compte
- **POST /api/stripe/accounts/:id/onboarding-link**: Liens onboarding
- **POST /api/stripe/locations**: Création locations Terminal
- **POST /api/stripe/terminal/connection-token**: Tokens connexion
- **POST /api/stripe/webhook**: Réception événements Stripe

### Interface Flutter (Frontend)
- Widget configuration Stripe dans amicale_form.dart
- Service StripeConnectService pour communication API
- États visuels dynamiques avec codes couleur
- Messages utilisateur "100% des paiements pour votre amicale"

## 🔧 Corrections techniques

### StripeController.php
- Fix Database::getInstance() → $this->db
- Fix $db->prepare() → $this->db->prepare()
- Suppression colonne details_submitted inexistante
- Ajout exit après réponses JSON (évite 502)

### StripeService.php
- Ajout imports Stripe SDK (use Stripe\Account)
- Fix Account::retrieve() → $this->stripe->accounts->retrieve()
- **CRUCIAL**: Déchiffrement données encrypted_email/encrypted_name
- Suppression calcul commission (0% plateforme)

### Router.php
- Suppression logs debug excessifs (fix nginx 502 "header too big")

### AppConfig.php
- application_fee_percent: 0 (était 2.5)
- application_fee_minimum: 0 (était 50)
- **POLITIQUE**: 100% des paiements vers amicales

##  Tests validés
- Compte pilote créé: acct_1S2YfNP63A07c33Y
- Location Terminal: tml_GLJ21w7KCYX4Wj
- Onboarding Stripe complété avec succès
- Toutes les APIs retournent 200 OK

## 📚 Documentation
- Plannings mis à jour avec accomplissements
- Architecture technique documentée
- Erreurs résolues listées avec solutions

## 🚀 Prêt pour production
V1 Stripe Connect opérationnelle - Prochaine étape: Terminal Payments V2

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-01 18:11:28 +02:00

435 lines
14 KiB
PHP
Executable File

<?php
declare(strict_types=1);
/**
* Configuration de l'application Geosector
*
* Ce fichier contient la configuration de l'application Geosector pour les trois environnements :
* - Production (app.geosector.fr)
* - Recette (rapp.geosector.fr)
* - Développement (dapp.geosector.fr)
*
* Il inclut les paramètres de base de données, les informations SMTP,
* les clés de chiffrement et les configurations des services externes (Mapbox, Stripe, SMS OVH).
*/
class AppConfig {
private static ?self $instance = null;
private array $headers;
private array $config;
private string $currentHost;
private string $clientIp;
private function __construct() {
// Récupération du host directement depuis SERVER_NAME ou HTTP_HOST
$this->currentHost = $_SERVER['SERVER_NAME'] ?? $_SERVER['HTTP_HOST'] ?? '';
// Récupérer les autres en-têtes pour une utilisation ultérieure si nécessaire
// getallheaders() n'existe pas en CLI, donc on vérifie
$this->headers = function_exists('getallheaders') ? getallheaders() : [];
// Déterminer l'adresse IP du client
$this->clientIp = $this->getClientIpAddress();
$this->initConfig();
$this->validateApp();
}
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function initConfig(): void {
// Configuration de base commune à tous les environnements
$baseConfig = [
'name' => 'geosector',
'encryption_key' => 'Qga2M8Ov6tyx2fIQRWHQ1U6oMK/bAFdTL7A8VRtiDhk=',
'smtp' => [
'host' => 'barbotte.o2switch.net',
'auth' => true,
'user' => 'noreply@geosector.fr',
'pass' => '@G83^[OMSo^Q',
'secure' => 'ssl',
'port' => 465,
],
'email' => [
'from' => 'noreply@geosector.fr',
'contact' => 'contact@geosector.fr',
'hourly_limit' => 1500, // Limite de 1500 emails/heure comme mentionné dans le cahier des charges
],
'mapbox' => [
'api_key' => '', // À remplir avec la clé API Mapbox
],
'stripe' => [
'public_key_test' => 'pk_test_51QwoVN00pblGEgsXkf8qlXmLGEpxDQcG0KLRpjrGLjJHd7AVZ4Iwd6ChgdjO0w0n3vRqwNCEW8KnHUe5eh3uIlkV00k07kCBmd', // À remplacer par votre clé publique TEST
'secret_key_test' => 'sk_test_51QwoVN00pblGEgsXnvqi8qfYpzHtesWWclvK3lzQjPNoHY0dIyOpJmxIkoLqsbmRMEUZpKS5MQ7iFDRlSqVyTo9c006yWetbsd', // À remplacer par votre clé secrète TEST
'public_key_live' => 'pk_live_XXXXXXXXXXXX', // À remplacer par votre clé publique LIVE
'secret_key_live' => 'sk_live_XXXXXXXXXXXX', // À remplacer par votre clé secrète LIVE
'webhook_secret_test' => 'whsec_test_XXXXXXXXXXXX', // À remplacer après création webhook TEST
'webhook_secret_live' => 'whsec_live_XXXXXXXXXXXX', // À remplacer après création webhook LIVE
'api_version' => '2024-06-20',
'application_fee_percent' => 0, // Pas de commission plateforme
'application_fee_minimum' => 0, // Pas de commission minimum
'mode' => 'test', // 'test' ou 'live'
],
'sms' => [
'provider' => 'ovh', // Comme mentionné dans le cahier des charges
'api_key' => '', // À remplir avec la clé API SMS OVH
'api_secret' => '', // À remplir avec le secret API SMS OVH
],
'backup' => [
'encryption_key' => 'K8mN2pQ5rT9wX3zA6bE1fH4jL7oS0vY2', // Clé de 32 caractères pour AES-256
'compression' => true,
'compression_level' => 6,
'cipher' => 'AES-256-CBC'
],
];
// Configuration PRODUCTION
$this->config['app.geosector.fr'] = array_merge($baseConfig, [
'env' => 'production',
'database' => [
'host' => 'localhost',
'name' => 'geo_app',
'username' => 'geo_app_user_prod',
'password' => 'QO:96-SrHJ6k7-df*?k{4W6m',
],
'addresses_database' => [
'host' => '13.23.33.26',
'name' => 'adresses',
'username' => 'adr_geo_user',
'password' => 'd66,AdrGeo.User',
],
]);
// Configuration RECETTE
$this->config['rapp.geosector.fr'] = array_merge($baseConfig, [
'env' => 'recette',
'database' => [
'host' => 'localhost',
'name' => 'geo_app',
'username' => 'geo_app_user_rec',
'password' => 'QO:96df*?k-dS3KiO-{4W6m',
],
'addresses_database' => [
'host' => '13.23.33.36',
'name' => 'adresses',
'username' => 'adr_geo_user',
'password' => 'd66,AdrGeoRec.User',
],
// Vous pouvez remplacer d'autres paramètres spécifiques à l'environnement de recette ici
]);
// Configuration DÉVELOPPEMENT
$this->config['dapp.geosector.fr'] = array_merge($baseConfig, [
'env' => 'development',
'database' => [
'host' => 'localhost',
'name' => 'geo_app',
'username' => 'geo_app_user_dev',
'password' => '34GOz-X5gJu-oH@Fa3$#Z',
],
'addresses_database' => [
'host' => '13.23.33.46',
'name' => 'adresses',
'username' => 'adr_geo_user',
'password' => 'd66,AdrGeoDev.User',
],
// Vous pouvez activer des fonctionnalités de débogage en développement
'debug' => true,
]);
}
private function validateApp(): void {
// Si l'hôte est vide, utiliser une solution de secours (développement par défaut)
if (empty($this->currentHost)) {
// Journaliser cette situation anormale
error_log("WARNING: No host detected, falling back to development environment");
$this->currentHost = 'dapp.geosector.fr';
}
// Si l'hôte n'existe pas dans la configuration, tenter une correction
if (!isset($this->config[$this->currentHost])) {
// Essayer de faire correspondre avec l'un des hôtes connus
$knownHosts = array_keys($this->config);
foreach ($knownHosts as $host) {
if (strpos($this->currentHost, str_replace(['app.', 'rapp.', 'dapp.'], '', $host)) !== false) {
// Correspondance trouvée, utiliser cette configuration
$this->currentHost = $host;
break;
}
}
// Si toujours pas de correspondance, utiliser l'environnement de développement par défaut
if (!isset($this->config[$this->currentHost])) {
error_log("WARNING: Unknown host '{$this->currentHost}', falling back to development environment");
$this->currentHost = 'dapp.geosector.fr';
}
}
// Journaliser l'environnement détecté
$environment = $this->config[$this->currentHost]['env'] ?? 'unknown';
// error_log("INFO: Environment detected: {$environment} (Host: {$this->currentHost}, IP: {$this->clientIp})");
}
/**
* Retourne le type de client (web, mobile, etc.)
*
* @return string Le type de client ou 'unknown' si non défini
*/
public function getClientType(): string {
return $this->headers['X-Client-Type'] ?? $_SERVER['HTTP_X_CLIENT_TYPE'] ?? 'unknown';
}
/**
* Retourne l'identifiant de l'application basé sur l'hôte
*
* @return string L'identifiant de l'application (app.geosector.fr, rapp.geosector.fr, dapp.geosector.fr)
*/
public function getAppIdentifier(): string {
return $this->currentHost;
}
/**
* Retourne l'environnement actuel (production, recette, development)
*
* @return string L'environnement actuel
*/
public function getEnvironment(): string {
return $this->getCurrentConfig()['env'] ?? 'production';
}
/**
* Vérifie si l'application est en mode développement
*
* @return bool True si l'application est en mode développement
*/
public function isDevelopment(): bool {
return $this->getEnvironment() === 'development';
}
/**
* Vérifie si l'application est en mode recette
*
* @return bool True si l'application est en mode recette
*/
public function isRecette(): bool {
return $this->getEnvironment() === 'recette';
}
/**
* Vérifie si l'application est en mode production
*
* @return bool True si l'application est en mode production
*/
public function isProduction(): bool {
return $this->getEnvironment() === 'production';
}
/**
* Retourne la configuration complète de l'environnement actuel
*
* @return array Configuration de l'environnement
*/
public function getCurrentConfig(): array {
return $this->config[$this->currentHost];
}
/**
* Retourne le nom de l'application
*
* @return string Nom de l'application (geosector)
*/
public function getName(): string {
return $this->getCurrentConfig()['name'];
}
/**
* Retourne la configuration de la base de données
*
* @return array Configuration de la base de données
*/
public function getDatabaseConfig(): array {
return $this->getCurrentConfig()['database'];
}
/**
* Retourne la configuration de la base de données des adresses
*
* @return array Configuration de la base de données des adresses
*/
public function getAddressesDatabaseConfig(): array {
return $this->getCurrentConfig()['addresses_database'];
}
/**
* Retourne la clé de chiffrement
*
* @return string Clé de chiffrement
*/
public function getEncryptionKey(): string {
return $this->getCurrentConfig()['encryption_key'];
}
/**
* Retourne la configuration SMTP
*
* @return array Configuration SMTP
*/
public function getSmtpConfig(): array {
return $this->getCurrentConfig()['smtp'];
}
/**
* Retourne la configuration email
*
* @return array Configuration email
*/
public function getEmailConfig(): array {
return $this->getCurrentConfig()['email'];
}
/**
* Retourne la configuration Mapbox
*
* @return array Configuration Mapbox
*/
public function getMapboxConfig(): array {
return $this->getCurrentConfig()['mapbox'];
}
/**
* Retourne la configuration Stripe
*
* @return array Configuration Stripe
*/
public function getStripeConfig(): array {
return $this->getCurrentConfig()['stripe'];
}
/**
* Retourne la configuration SMS
*
* @return array Configuration SMS
*/
public function getSmsConfig(): array {
return $this->getCurrentConfig()['sms'];
}
/**
* Retourne si le mode debug est activé
*
* @return bool True si le mode debug est activé
*/
public function isDebugEnabled(): bool {
return $this->getCurrentConfig()['debug'] ?? false;
}
/**
* Retourne la liste des origines autorisées (domaines)
*
* @return array Liste des origines autorisées
*/
public function getAllowedOrigins(): array {
return array_keys($this->config);
}
/**
* Vérifie si le client est d'un type spécifique
*
* @param string $type Type de client à vérifier
* @return bool True si le client est du type spécifié
*/
public function isClientType(string $type): bool {
return $this->getClientType() === $type;
}
/**
* Retourne la configuration complète pour l'utilisation externe
*
* @return array Configuration complète
*/
public function getFullConfig(): array {
return [
'environment' => $this->getEnvironment(),
'database' => $this->getDatabaseConfig(),
'api' => [
'allowed_origins' => $this->getAllowedOrigins(),
'current_site' => $this->getName(),
],
'debug' => $this->isDebugEnabled()
];
}
/**
* Retourne l'adresse IP du client
*
* @return string L'adresse IP du client
*/
public function getClientIp(): string {
return $this->clientIp;
}
/**
* Retourne la configuration des backups
*
* @return array Configuration des backups
*/
public function getBackupConfig(): array {
return $this->getCurrentConfig()['backup'];
}
/**
* Retourne la clé de chiffrement des backups
*
* @return string Clé de chiffrement des backups
*/
public function getBackupEncryptionKey(): string {
return $this->getCurrentConfig()['backup']['encryption_key'];
}
/**
* Détermine l'adresse IP du client en tenant compte des proxys et load balancers
*
* @return string L'adresse IP du client
*/
private function getClientIpAddress(): string {
// Vérifier les en-têtes courants pour l'IP client
$ipSources = [
'HTTP_X_REAL_IP', // Nginx proxy
'HTTP_CLIENT_IP', // Proxy partagé
'HTTP_X_FORWARDED_FOR', // Proxy ou load balancer courant
'HTTP_X_FORWARDED', // Proxy générique
'HTTP_X_CLUSTER_CLIENT_IP', // Reverse proxy
'HTTP_FORWARDED_FOR', // Proxies précédents
'HTTP_FORWARDED', // Format standardisé (RFC 7239)
'REMOTE_ADDR', // Fallback direct
];
foreach ($ipSources as $source) {
if (!empty($_SERVER[$source])) {
// Pour des en-têtes comme X-Forwarded-For qui peuvent contenir plusieurs IPs séparées par des virgules
// (format: "client, proxy1, proxy2")
if ($source === 'HTTP_X_FORWARDED_FOR' || $source === 'HTTP_FORWARDED_FOR') {
$ips = explode(',', $_SERVER[$source]);
$clientIp = trim($ips[0]); // Prendre la première adresse (client original)
} else {
$clientIp = $_SERVER[$source];
}
// Valider l'IP pour éviter les injections
$filteredIp = filter_var($clientIp, FILTER_VALIDATE_IP);
if ($filteredIp !== false) {
return $filteredIp;
}
}
}
// Si aucune adresse IP valide n'est trouvée, retourner une valeur par défaut
return '0.0.0.0';
}
}