veza/docs/PLAN_V0_603_IMPLEMENTATION.md
senke c4110fded7 docs(v0.603): scope, plan d'implémentation et smoke test
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.
2026-02-23 22:48:04 +01:00

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 :

  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 :

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_*.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 :

## 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.mddocs/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

# 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