Define v0.603 release scope: automatic Stripe Connect transfers after payment, configurable platform commission, technical debt triage (210+ TODOs), and docs archival. Includes detailed implementation plan (4 sprints, 19 commits) and smoke test checklist.
20 KiB
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
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
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 :
PlatformFeeRate float64 // PLATFORM_FEE_RATE, default 0.10 (10%)
Chargement dans LoadConfig() :
cfg.PlatformFeeRate = getEnvFloat("PLATFORM_FEE_RATE", 0.10)
Fichier : .env.example
# 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
-- 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 :
// 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 :
// 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 :
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 :
// 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 :
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 :
// 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 :
// 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 :
// 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
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
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 :
- TestProcessWebhook_TransferSuccess — paiement réussi → license créée + transfer
completedenregistré - TestProcessWebhook_MultiSeller — 2 items de 2 vendeurs → 2
SellerTransferdistincts - TestProcessWebhook_SellerNoConnect — vendeur sans compte Connect →
CreateTransferretourneErrNoStripeAccount→ transferfailedenregistré, commande quand mêmecompleted
Mock TransferService :
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 :
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_*.mdV0_201_*.md,V0_202_*.md,V0_203_*.mdV0_301_*.md,V0_302_*.md,V0_303_*.mdV0_401_*.md,V0_402_*.md,V0_403_*.mdPLAN_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 :
## 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 :
- Créer un user vendeur avec
seller_stripe_accounts(PayoutsEnabled = true) - Créer un produit de ce vendeur
- Créer une commande (buyer, 1 item)
- Simuler webhook
succeededavecProcessPaymentWebhook - Vérifier : order
completed, license créée,SellerTransferenregistré avecstatus = 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
SellerTransferdistincts, 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,
SellerTransferavecstatus = failed, commandecompleted
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éedocs/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
- Créer
docs/RETROSPECTIVE_V0603.md - Déplacer
docs/V0_603_RELEASE_SCOPE.md→docs/archive/ - Créer placeholder
docs/V0_604_RELEASE_SCOPE.md - Mettre à jour
docs/SCOPE_CONTROL.md— référence active → V0_604 - Tag :
git tag -a v0.603 -m "v0.603 — Transfer automatique, Commission & Stabilisation"
Commits :
docs: add RETROSPECTIVE_V0603.mdchore(release): archive v0.603 scope, create v0.604 placeholderchore(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
# 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