# Plan d'implémentation v0.603 — Transfer automatique, Commission & Stabilisation **Date** : 2026-02-23 **Base** : v0.602 taguée **Durée estimée** : 4 sprints (~20 jours ouvrés) **Référence** : [V0_603_RELEASE_SCOPE.md](V0_603_RELEASE_SCOPE.md) --- ## Vue d'ensemble ``` Sprint 1 (j1-5) → T1-01..T1-05 : Config, migration, modèle, interface, injection Sprint 2 (j6-12) → T1-06..T1-10 : Logique transfer, endpoint, frontend, tests Sprint 3 (j13-17) → DT1 : Triage TODOs, archivage docs, nettoyage Sprint 4 (j18-20) → QA3 : Tests E2E, smoke test, docs, rétro, tag ``` --- ## Diagramme d'architecture cible ```mermaid flowchart TD subgraph Webhook["Hyperswitch Webhook (payment.succeeded)"] PW["ProcessPaymentWebhook"] end subgraph Transfer["Transfer Flow (nouveau v0.603)"] Group["Regrouper items par seller_id"] Calc["Calculer montant net = prix - commission"] Check["Vendeur a un compte Connect ?"] Yes["CreateTransfer (Stripe API)"] No["Log warning, status = skipped"] Record["Enregistrer SellerTransfer en DB"] end subgraph Existing["Existant (v0.602)"] License["Créer licences"] Order["Order status = completed"] end PW --> Order PW --> License License --> Group Group --> Calc Calc --> Check Check -->|Oui| Yes Check -->|Non| No Yes --> Record No --> Record ``` --- ## Sprint 1 — Config, migration, modèle, injection (jours 1-5) > **Objectif** : Préparer l'infrastructure backend pour les transferts. ### Tâche T1-01 : Config commission plateforme **Fichier** : `veza-backend-api/internal/config/config.go` Ajouter dans la struct Config : ```go PlatformFeeRate float64 // PLATFORM_FEE_RATE, default 0.10 (10%) ``` Chargement dans `LoadConfig()` : ```go cfg.PlatformFeeRate = getEnvFloat("PLATFORM_FEE_RATE", 0.10) ``` **Fichier** : `.env.example` ```env # Platform commission on marketplace sales (0.10 = 10%) PLATFORM_FEE_RATE=0.10 ``` **Commit** : `feat(commerce): add PLATFORM_FEE_RATE config (default 10%)` --- ### Tâche T1-02 : Migration seller_transfers **Fichier** : `veza-backend-api/migrations/115_seller_transfers.sql` ```sql -- v0.603 T1-02: Seller transfer tracking CREATE TABLE IF NOT EXISTS seller_transfers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), seller_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE, stripe_transfer_id VARCHAR(255), amount_cents BIGINT NOT NULL, platform_fee_cents BIGINT NOT NULL, currency VARCHAR(3) NOT NULL DEFAULT 'EUR', status VARCHAR(50) NOT NULL DEFAULT 'pending', error_message TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_seller_transfers_seller ON seller_transfers(seller_id); CREATE INDEX IF NOT EXISTS idx_seller_transfers_order ON seller_transfers(order_id); CREATE UNIQUE INDEX IF NOT EXISTS idx_seller_transfers_seller_order ON seller_transfers(seller_id, order_id); ``` **Commit** : `feat(commerce): add 115_seller_transfers migration` --- ### Tâche T1-03 : Modèle SellerTransfer **Fichier** : `veza-backend-api/internal/core/marketplace/models.go` Ajouter après le type `ProductReview` : ```go // SellerTransfer tracks a Stripe Connect transfer for a completed order. type SellerTransfer struct { ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"` SellerID uuid.UUID `gorm:"type:uuid;not null" json:"seller_id"` OrderID uuid.UUID `gorm:"type:uuid;not null" json:"order_id"` StripeTransferID string `gorm:"size:255" json:"stripe_transfer_id,omitempty"` AmountCents int64 `gorm:"not null" json:"amount_cents"` PlatformFeeCents int64 `gorm:"not null" json:"platform_fee_cents"` Currency string `gorm:"size:3;default:'EUR'" json:"currency"` Status string `gorm:"size:50;default:'pending'" json:"status"` // pending, completed, failed, skipped ErrorMessage string `gorm:"type:text" json:"error_message,omitempty"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } func (st *SellerTransfer) BeforeCreate(tx *gorm.DB) (err error) { if st.ID == uuid.Nil { st.ID = uuid.New() } return } ``` **Commit** : `feat(commerce): add SellerTransfer model` --- ### Tâche T1-04 : Interface TransferService **Fichier** : `veza-backend-api/internal/core/marketplace/service.go` Ajouter avant le type `Service struct` : ```go // TransferService abstracts the payout transfer provider. type TransferService interface { CreateTransfer(ctx context.Context, sellerUserID uuid.UUID, amount int64, currency, orderID string) error } ``` **Commit** : combiné avec T1-05 --- ### Tâche T1-05 : Option WithTransferService **Fichier** : `veza-backend-api/internal/core/marketplace/service.go` Ajouter les champs dans `Service struct` : ```go type Service struct { db *gorm.DB logger *zap.Logger storage StorageService paymentProvider PaymentProvider hyperswitchEnabled bool checkoutSuccessURL string transferService TransferService // v0.603: optional payout transfer platformFeeRate float64 // v0.603: platform commission rate (e.g. 0.10) } ``` Ajouter la `ServiceOption` : ```go // WithTransferService injects the payout transfer service and platform fee rate. func WithTransferService(ts TransferService, feeRate float64) ServiceOption { return func(s *Service) { s.transferService = ts s.platformFeeRate = feeRate } } ``` **Fichier** : Wiring — là où `marketplace.NewService` est appelé (probablement `cmd/server/main.go` ou `internal/api/setup.go`), ajouter l'option : ```go marketplace.WithTransferService(stripeConnectService, cfg.PlatformFeeRate) ``` **Commit** : `feat(commerce): add TransferService interface and WithTransferService option` --- ## Sprint 2 — Logique transfer, endpoint, frontend (jours 6-12) > **Objectif** : Implémenter le flux transfer dans le webhook et exposer l'historique. ### Tâche T1-06 : Logique transfer dans ProcessPaymentWebhook **Fichier** : `veza-backend-api/internal/core/marketplace/service.go` Dans la méthode `ProcessPaymentWebhook`, dans le case `"succeeded"`, **après** la boucle de création des licences, ajouter : ```go // v0.603 T1-06: Transfer to sellers after license creation if s.transferService != nil { s.processSellerTransfers(ctx, tx, &order, items) } ``` Nouvelle méthode privée `processSellerTransfers` : ```go // processSellerTransfers groups order items by seller and initiates transfers. // Errors are logged and recorded but do not fail the order. func (s *Service) processSellerTransfers(ctx context.Context, tx *gorm.DB, order *Order, items []OrderItem) { // 1. Group items by seller, sum prices sellerTotals := make(map[uuid.UUID]float64) for _, item := range items { var product Product if err := tx.First(&product, "id = ?", item.ProductID).Error; err != nil { s.logger.Warn("Transfer: product not found for item", zap.String("item_id", item.ID.String())) continue } sellerTotals[product.SellerID] += item.Price } // 2. For each seller, calculate net amount and transfer for sellerID, totalPrice := range sellerTotals { grossCents := int64(totalPrice * 100) feeCents := int64(float64(grossCents) * s.platformFeeRate) netCents := grossCents - feeCents st := SellerTransfer{ SellerID: sellerID, OrderID: order.ID, AmountCents: netCents, PlatformFeeCents: feeCents, Currency: order.Currency, Status: "pending", } err := s.transferService.CreateTransfer(ctx, sellerID, netCents, order.Currency, order.ID.String()) if err != nil { st.Status = "failed" st.ErrorMessage = err.Error() s.logger.Error("Transfer failed for seller", zap.String("seller_id", sellerID.String()), zap.String("order_id", order.ID.String()), zap.Error(err)) } else { st.Status = "completed" s.logger.Info("Transfer completed for seller", zap.String("seller_id", sellerID.String()), zap.Int64("amount_cents", netCents)) } if err := tx.Create(&st).Error; err != nil { s.logger.Error("Failed to record seller transfer", zap.String("seller_id", sellerID.String()), zap.Error(err)) } } } ``` **Commit** : `feat(commerce): trigger seller transfers on payment succeeded` --- ### Tâche T1-07 : Endpoint GET /sell/transfers **Fichier** : `veza-backend-api/internal/handlers/sell_handler.go` Ajouter le handler `GetSellerTransfers` : ```go // GetSellerTransfers returns the transfer history for the authenticated seller. // GET /api/v1/sell/transfers func (h *SellHandler) GetSellerTransfers(c *gin.Context) { userID := getUserID(c) var transfers []marketplace.SellerTransfer if err := h.db.Where("seller_id = ?", userID). Order("created_at DESC"). Find(&transfers).Error; err != nil { c.JSON(500, gin.H{"error": "failed to fetch transfers"}) return } c.JSON(200, gin.H{"data": transfers}) } ``` **Fichier** : routes — ajouter `GET /sell/transfers` dans le groupe sell authentifié. **Commit** : `feat(commerce): add GET /sell/transfers endpoint` --- ### Tâche T1-08 : Frontend — SellerTransfersCard **Fichier** : `apps/web/src/features/seller/SellerDashboardView.tsx` Ajouter une carte « Historique des transferts » dans le dashboard vendeur : - Appel `GET /sell/transfers` - Liste : date, montant, commission, statut (badge coloré) - États : Loading (skeleton), Empty (« Aucun transfert »), Error **Fichier** : `apps/web/src/services/marketplaceService.ts` ```typescript export const getSellerTransfers = () => apiClient.get<{ data: SellerTransfer[] }>('/sell/transfers'); export interface SellerTransfer { id: string; order_id: string; amount_cents: number; platform_fee_cents: number; currency: string; status: 'pending' | 'completed' | 'failed' | 'skipped'; error_message?: string; created_at: string; } ``` **Commit** : `feat(seller): add transfers history card to SellerDashboard` --- ### Tâche T1-09 : MSW handlers + Story **Fichier** : `apps/web/src/mocks/handlers.ts` ```typescript http.get('/api/v1/sell/transfers', () => { return HttpResponse.json({ data: [ { id: '...', order_id: '...', amount_cents: 900, platform_fee_cents: 100, currency: 'EUR', status: 'completed', created_at: '2026-02-23T10:00:00Z', }, ], }); }), ``` **Story** : `SellerDashboardView.stories.tsx` — ajouter un état « Avec transferts » montrant la carte remplie. **Commit** : `test(seller): add MSW handler and story for transfers` --- ### Tâche T1-10 : Tests unitaires transfer **Fichier** : `veza-backend-api/internal/core/marketplace/process_webhook_test.go` Ajouter 3 cas de test : 1. **TestProcessWebhook_TransferSuccess** — paiement réussi → license créée + transfer `completed` enregistré 2. **TestProcessWebhook_MultiSeller** — 2 items de 2 vendeurs → 2 `SellerTransfer` distincts 3. **TestProcessWebhook_SellerNoConnect** — vendeur sans compte Connect → `CreateTransfer` retourne `ErrNoStripeAccount` → transfer `failed` enregistré, commande quand même `completed` Mock `TransferService` : ```go type mockTransferService struct { callCount int err error } func (m *mockTransferService) CreateTransfer(ctx context.Context, sellerUserID uuid.UUID, amount int64, currency, orderID string) error { m.callCount++ return m.err } ``` **Commit** : `test(commerce): add transfer tests — success, multi-seller, no-connect` --- ## Sprint 3 — Dette technique & Nettoyage (jours 13-17) > **Objectif** : Triage des TODOs, archivage des docs obsolètes. ### Tâche DT1-01 : Triage TODOs backend **Procédure** : ```bash cd veza-backend-api rg -n "TODO|FIXME" --type go | sort > /tmp/todos.txt wc -l /tmp/todos.txt # ~210 ``` Pour chaque TODO : - **Obsolète** (code déjà refactoré, feature livrée) → supprimer le commentaire - **Pertinent** (bug connu, amélioration future) → créer une issue GitHub, garder un `// TODO(#NNN): ...` - **Objectif** : réduire de 210 à < 100 **Commit** : `chore(backend): triage TODOs — remove 100+ obsolete, convert rest to issues` --- ### Tâche DT1-02 : Archiver docs obsolètes **Déplacer vers** `docs/archive/` les docs pré-v0.501 qui ne sont plus des références actives : - `V0_101_*.md`, `V0_102_*.md`, `V0_103_*.md` - `V0_201_*.md`, `V0_202_*.md`, `V0_203_*.md` - `V0_301_*.md`, `V0_302_*.md`, `V0_303_*.md` - `V0_401_*.md`, `V0_402_*.md`, `V0_403_*.md` - `PLAN_V0_301_*.md`, `PLAN_V0_401_*.md`, `PLAN_V0_402_*.md`, `PLAN_V0_403_*.md` - Anciens audits : `AUDIT_TEMP_*.md`, `DB_MIGRATIONS_AUDIT_*.md`, `UUID_DB_*.md`, `AUDIT_DB_*.md` **Ne pas archiver** : `SCOPE_CONTROL.md`, `PROJECT_STATE.md`, `FEATURE_STATUS.md`, `MIGRATIONS.md`, `ENV_CONFIG.md`, `ONBOARDING.md`, `MONITORING_SETUP.md`, `STORYBOOK_CONTRACT.md`, plans v0.601+. **Commit** : `chore(docs): archive obsolete pre-v0.501 docs` --- ### Tâche DT1-03 : Mettre à jour PAYOUT_MANUAL.md **Fichier** : `docs/PAYOUT_MANUAL.md` Remplacer la section « Procédure manuelle » par : ```markdown ## Implémentation v0.603 Le transfert automatique est opérationnel depuis v0.603 : 1. Paiement réussi (webhook Hyperswitch `succeeded`) 2. Licences créées pour l'acheteur 3. Items regroupés par vendeur 4. Commission plateforme déduite (PLATFORM_FEE_RATE) 5. Transfer Stripe Connect exécuté vers le compte du vendeur 6. SellerTransfer enregistré en DB (table seller_transfers) Voir : V0_603_RELEASE_SCOPE.md § 5 pour le détail technique. ``` **Commit** : `docs(payout): update PAYOUT_MANUAL for v0.603 auto transfer` --- ### Tâche DT1-04 : Nettoyage code mort marketplace **Fichier** : `veza-backend-api/internal/core/marketplace/` - Vérifier imports inutilisés - Vérifier variables déclarées non utilisées - Lancer `go vet ./internal/core/marketplace/...` **Commit** : `chore(marketplace): remove dead code and unused imports` --- ## Sprint 4 — Tests E2E, docs, tag (jours 18-20) > **Objectif** : Validation complète, documentation, release. ### Tâche QA3-01 : Test E2E transfer **Fichier** : `veza-backend-api/internal/core/marketplace/process_webhook_test.go` ou nouveau fichier d'intégration. Test complet dans une transaction : 1. Créer un user vendeur avec `seller_stripe_accounts` (PayoutsEnabled = true) 2. Créer un produit de ce vendeur 3. Créer une commande (buyer, 1 item) 4. Simuler webhook `succeeded` avec `ProcessPaymentWebhook` 5. Vérifier : order `completed`, license créée, `SellerTransfer` enregistré avec `status = completed` **Commit** : `test(commerce): add E2E transfer flow test` --- ### Tâche QA3-02 : Test multi-vendeur Même setup que QA3-01 mais : - 2 vendeurs avec comptes Connect - 1 commande avec 2 items (1 par vendeur) - Vérifier : 2 `SellerTransfer` distincts, montants corrects **Commit** : combiné avec QA3-01 --- ### Tâche QA3-03 : Test vendeur sans Connect - 1 vendeur **sans** entrée dans `seller_stripe_accounts` - 1 commande, webhook `succeeded` - Vérifier : licences créées, `SellerTransfer` avec `status = failed`, commande `completed` **Commit** : combiné avec QA3-01 --- ### Tâche QA3-04 : Smoke test v0.603 **Fichier** : `docs/SMOKE_TEST_V0603.md` — voir document dédié. **Commit** : `docs: add SMOKE_TEST_V0603.md` --- ### Tâche QA3-05 : Mise à jour documentation **Fichiers** : - `docs/PROJECT_STATE.md` — ajouter section v0.603 livrée - `docs/FEATURE_STATUS.md` — Marketplace → « Transfer automatique opérationnel (v0.603) » - `CHANGELOG.md` — section v0.603 **Commit** : `docs: update PROJECT_STATE, FEATURE_STATUS, CHANGELOG for v0.603` --- ### Tâche QA3-06 : Rétrospective, archivage, tag 1. Créer `docs/RETROSPECTIVE_V0603.md` 2. Déplacer `docs/V0_603_RELEASE_SCOPE.md` → `docs/archive/` 3. Créer placeholder `docs/V0_604_RELEASE_SCOPE.md` 4. Mettre à jour `docs/SCOPE_CONTROL.md` — référence active → V0_604 5. Tag : `git tag -a v0.603 -m "v0.603 — Transfer automatique, Commission & Stabilisation"` **Commits** : - `docs: add RETROSPECTIVE_V0603.md` - `chore(release): archive v0.603 scope, create v0.604 placeholder` - `chore(release): v0.603 — Transfer automatique, Commission & Stabilisation` --- ## Commits récapitulatifs (ordre d'exécution) | # | Sprint | Commit | |---|--------|--------| | 1 | 1 | `feat(commerce): add PLATFORM_FEE_RATE config (default 10%)` | | 2 | 1 | `feat(commerce): add 115_seller_transfers migration` | | 3 | 1 | `feat(commerce): add SellerTransfer model` | | 4 | 1 | `feat(commerce): add TransferService interface and WithTransferService option` | | 5 | 2 | `feat(commerce): trigger seller transfers on payment succeeded` | | 6 | 2 | `feat(commerce): add GET /sell/transfers endpoint` | | 7 | 2 | `feat(seller): add transfers history card to SellerDashboard` | | 8 | 2 | `test(seller): add MSW handler and story for transfers` | | 9 | 2 | `test(commerce): add transfer tests — success, multi-seller, no-connect` | | 10 | 3 | `chore(backend): triage TODOs — remove 100+ obsolete, convert rest to issues` | | 11 | 3 | `chore(docs): archive obsolete pre-v0.501 docs` | | 12 | 3 | `docs(payout): update PAYOUT_MANUAL for v0.603 auto transfer` | | 13 | 3 | `chore(marketplace): remove dead code and unused imports` | | 14 | 4 | `test(commerce): add E2E transfer flow test` | | 15 | 4 | `docs: add SMOKE_TEST_V0603.md` | | 16 | 4 | `docs: update PROJECT_STATE, FEATURE_STATUS, CHANGELOG for v0.603` | | 17 | 4 | `docs: add RETROSPECTIVE_V0603.md` | | 18 | 4 | `chore(release): archive v0.603 scope, create v0.604 placeholder` | | 19 | 4 | `chore(release): v0.603 — Transfer automatique, Commission & Stabilisation` | --- ## Dépendances entre lots ``` T1-01..T1-05 (Config/Migration/Modèle) → aucune dépendance T1-06 (Logique transfer) → dépend de T1-01..T1-05 T1-07 (Endpoint) → dépend de T1-03 T1-08..T1-09 (Frontend) → dépend de T1-07 T1-10 (Tests unitaires) → dépend de T1-06 DT1 (Dette technique) → indépendant (peut démarrer en parallèle) QA3 (Tests E2E, docs) → dépend de T1, DT1 ``` --- ## Risques et mitigations | Risque | Mitigation | |--------|------------| | Stripe rate limit sur CreateTransfer | 1 transfer par vendeur par commande, pas par item | | Échec transfer silencieux | Enregistré en DB, log `zap.Error`, alertable via Grafana/Alertmanager | | Float→int64 precision loss | Conversion `int64(price * 100)` au plus tôt, calculs en centimes | | TODO triage trop long | Timebox à 2 jours max, focus sur `internal/core/` et `internal/handlers/` | | Migration 115 conflict | Vérifier dernier numéro avant de commencer | --- ## Validation finale ```bash # Backend cd veza-backend-api && go build ./... cd veza-backend-api && go test ./... -v cd veza-backend-api && go test ./internal/core/marketplace/... -v -run TestProcessWebhook # Frontend cd apps/web && npm run build cd apps/web && npm test -- --run # Storybook cd apps/web && npm run build-storybook cd apps/web && npx http-server storybook-static -p 6007 & cd apps/web && npm run test:storybook ```