# Plan d'implémentation v0.702 — Reviews, Factures, Remboursements & Product Detail ## État des lieux Les fonctionnalités reviews (R1), factures (F1) et remboursements (R2) sont **déjà implémentées** : | Feature | Backend | Frontend service | Frontend UI | Route | MSW | Tests backend | |---------|---------|-----------------|-------------|-------|-----|---------------| | Reviews | ✅ service, handler, route | ✅ createReview, listReviews | ✅ ProductDetailViewReviews, ReviewProductModal | ❌ `/marketplace/products/:id` manque | ❌ | ❌ | | Invoices | ✅ invoice.go, handler, route | ✅ downloadInvoice | ✅ PurchasesViewItem, LicensesView | ✅ (via purchases) | ❌ | ❌ | | Refunds | ✅ RefundOrder, handler, route | ✅ refundOrder | ✅ RefundRequestModal | ✅ (via purchases) | ✅ | ❌ | **Ce plan complète le câblage, les tests et la documentation.** --- ## Fichiers existants clés - Models : [`models.go`](veza-backend-api/internal/core/marketplace/models.go) (ProductReview ligne 212, Order ligne 170, License ligne 119) - Service : [`service.go`](veza-backend-api/internal/core/marketplace/service.go) (CreateReview L1026, ListReviews L1071, RefundOrder L1098) - Invoice : [`invoice.go`](veza-backend-api/internal/core/marketplace/invoice.go) (GenerateInvoice L17, buildInvoicePDF L44) - Handlers : [`marketplace.go`](veza-backend-api/internal/handlers/marketplace.go) (CreateReview L314, ListReviews L350, GetOrderInvoice L791, RefundOrder L751) - Routes : [`routes_marketplace.go`](veza-backend-api/internal/api/routes_marketplace.go) - Frontend service : [`marketplaceService.ts`](apps/web/src/services/marketplaceService.ts) (createReview L235, listReviews L248, downloadInvoice L166, refundOrder L227) - ProductDetailView : [`ProductDetailView.tsx`](apps/web/src/components/marketplace/product-detail-view/ProductDetailView.tsx) - Reviews UI : [`ProductDetailViewReviews.tsx`](apps/web/src/components/marketplace/product-detail-view/ProductDetailViewReviews.tsx) - ReviewModal : [`ReviewProductModal.tsx`](apps/web/src/components/marketplace/modals/ReviewProductModal.tsx) - PurchasesView : [`PurchasesView.tsx`](apps/web/src/features/purchases/pages/purchases-page/PurchasesView.tsx) - MSW marketplace : [`handlers-marketplace.ts`](apps/web/src/mocks/handlers-marketplace.ts) (353 lignes) - Tests webhook : [`process_webhook_test.go`](veza-backend-api/internal/core/marketplace/process_webhook_test.go) - Stories existantes : [`ProductDetailView.stories.tsx`](apps/web/src/components/marketplace/ProductDetailView.stories.tsx) - Lazy exports : [`lazyExports.ts`](apps/web/src/components/ui/lazy-component/lazyExports.ts) - Routes : [`routeConfig.tsx`](apps/web/src/router/routeConfig.tsx) --- ## Sprint 1 : Lot W1 — Product Detail Page route ### Step 1 : ProductDetailPage + lazy export + route **Fichier** : `apps/web/src/features/marketplace/pages/ProductDetailPage.tsx` (nouveau) ```typescript import { useParams, useNavigate } from 'react-router-dom'; import { useState, useEffect } from 'react'; import { marketplaceService } from '@/services/marketplaceService'; import { ProductDetailView, ProductDetailViewSkeleton } from '@/components/marketplace/ProductDetailView'; import { ErrorDisplay } from '@/components/ui/ErrorDisplay'; import { useCartStore } from '@/stores/cartStore'; import type { Product } from '@/types'; export function ProductDetailPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const addItem = useCartStore((s) => s.addItem); const [product, setProduct] = useState(null); const [similar, setSimilar] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!id) return; setLoading(true); setError(null); Promise.all([ marketplaceService.getProduct(id), marketplaceService.listProducts({}, { page: 1, limit: 4 }), ]) .then(([prod, res]) => { setProduct(prod); setSimilar((res.products || []).filter((p) => p.id !== id).slice(0, 3)); }) .catch((e) => setError(e instanceof Error ? e : new Error(String(e)))) .finally(() => setLoading(false)); }, [id]); if (loading) return ; if (error) return window.location.reload()} />; if (!product) return ; return ( navigate('/marketplace')} onAddToCart={() => addItem({ product_id: product.id, quantity: 1 })} /> ); } ``` **Fichier** : [`lazyExports.ts`](apps/web/src/components/ui/lazy-component/lazyExports.ts) — ajouter après `LazyMarketplace` : ```typescript export const LazyProductDetail = createLazyComponent( () => import('@/features/marketplace/pages/ProductDetailPage').then((m) => ({ default: m.ProductDetailPage, })), undefined, 'Product Detail', ); ``` **Fichier** : [`index.ts`](apps/web/src/components/ui/lazy-component/index.ts) — ajouter `LazyProductDetail` dans les exports **Fichier** : [`LazyComponent.tsx`](apps/web/src/components/ui/LazyComponent.tsx) — ajouter `LazyProductDetail` dans les re-exports **Fichier** : [`routeConfig.tsx`](apps/web/src/router/routeConfig.tsx) : - Ajouter `LazyProductDetail` dans les imports - Ajouter la route après `/marketplace` : ```typescript { path: '/marketplace/products/:id', element: wrapProtected() }, ``` **Commit** : `feat(marketplace): add ProductDetailPage, lazy export, route /marketplace/products/:id` --- ## Sprint 2 : Lot M1 — MSW handlers reviews & invoice ### Step 2 : MSW handlers reviews **Fichier** : [`handlers-marketplace.ts`](apps/web/src/mocks/handlers-marketplace.ts) — ajouter après le handler `GET /marketplace/products/:id` : ```typescript // v0.702: Product reviews http.get('*/api/v1/marketplace/products/:id/reviews', () => { return HttpResponse.json({ success: true, data: { reviews: [ { id: 'rev-1', product_id: 'prod-1', buyer_id: 'user-1', order_id: 'ord-1', rating: 5, comment: 'Amazing track, perfect for my project!', created_at: new Date(Date.now() - 86400000).toISOString(), }, { id: 'rev-2', product_id: 'prod-1', buyer_id: 'user-2', order_id: 'ord-2', rating: 4, comment: 'Good quality, a bit short though.', created_at: new Date(Date.now() - 172800000).toISOString(), }, ], }, }); }), http.post('*/api/v1/marketplace/products/:id/reviews', async ({ request }) => { const body = (await request.json()) as { rating?: number; comment?: string }; return HttpResponse.json({ success: true, data: { id: 'rev-new', product_id: 'prod-1', buyer_id: 'user-1', order_id: 'ord-1', rating: body.rating ?? 5, comment: body.comment ?? '', created_at: new Date().toISOString(), }, }, { status: 201 }); }), ``` ### Step 3 : MSW handler invoice **Fichier** : [`handlers-marketplace.ts`](apps/web/src/mocks/handlers-marketplace.ts) — ajouter après le handler `GET /marketplace/orders/:id` : ```typescript // v0.702: Invoice PDF download (mock blob) http.get('*/api/v1/marketplace/orders/:id/invoice', () => { const pdfContent = '%PDF-1.4 mock invoice content'; return new HttpResponse(pdfContent, { headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': 'attachment; filename="invoice.pdf"', }, }); }), ``` **Commit** : `feat(mocks): add MSW handlers for product reviews and invoice download` --- ## Sprint 3 : Lot T1 — Tests backend ### Step 4 : Tests reviews **Fichier** : `veza-backend-api/internal/core/marketplace/review_test.go` (nouveau) Cas de test (utiliser le pattern SQLite in-memory de `process_webhook_test.go`) : - `TestCreateReview_Success` : acheteur avec licence crée une review → status 200, review enregistrée - `TestCreateReview_InvalidRating` : rating=0 ou rating=6 → erreur - `TestCreateReview_NotPurchased` : acheteur sans licence → `ErrReviewNotPurchased` - `TestCreateReview_DuplicateReview` : 2ème review du même acheteur sur même produit → `ErrReviewAlreadyExists` - `TestListReviews_Paginated` : 3 reviews, limit=2 → retourne 2 reviews, `created_at DESC` - `TestListReviews_EmptyProduct` : produit sans reviews → slice vide, pas d'erreur Setup commun : - AutoMigrate: Product, Order, OrderItem, License, ProductLicense, ProductReview - Créer product + seller + buyer + order + license pour les tests positifs **Commit** : `test(marketplace): add product review unit tests` ### Step 5 : Tests invoices **Fichier** : `veza-backend-api/internal/core/marketplace/invoice_test.go` (nouveau) Cas de test : - `TestGenerateInvoice_Success` : order avec items → PDF bytes non vide, commence par `%PDF` - `TestGenerateInvoice_WrongBuyer` : buyer_id != order.buyer_id → erreur - `TestGenerateInvoice_OrderNotFound` : UUID inexistant → erreur - `TestGenerateInvoice_WithDiscount` : order avec `DiscountAmountCents > 0` → PDF inclut le rabais Setup : AutoMigrate Order, OrderItem, Product, créer order avec items. Note : `GenerateInvoice` utilise `github.com/go-pdf/fpdf`. Vérifier que cette dépendance est dans `go.mod`. **Commit** : `test(marketplace): add invoice generation unit tests` ### Step 6 : Tests refunds **Fichier** : `veza-backend-api/internal/core/marketplace/refund_test.go` (nouveau) Cas de test : - `TestRefundOrder_Success` : order completed avec HyperswitchPaymentID → status `refunded`, licences `revoked_at` set - `TestRefundOrder_NotCompleted` : order `pending` → `ErrOrderNotRefundable` - `TestRefundOrder_NoPaymentID` : order completed mais `HyperswitchPaymentID=""` → `ErrOrderNotRefundable` - `TestRefundOrder_Forbidden` : initiatorID != buyer ni seller → `ErrRefundForbidden` - `TestRefundOrder_SellerCanRefund` : seller initie le refund → succès Mock `refundProvider` : ```go type mockRefundProvider struct { err error } func (m *mockRefundProvider) Refund(_ context.Context, _ string, _ *int64, _ string) error { return m.err } func (m *mockRefundProvider) CreatePayment(ctx context.Context, req marketplace.PaymentRequest) (*marketplace.PaymentResponse, error) { return &marketplace.PaymentResponse{}, nil } func (m *mockRefundProvider) GetPayment(ctx context.Context, paymentID string) (*marketplace.PaymentStatus, error) { return &marketplace.PaymentStatus{}, nil } ``` Note : le mock doit implémenter `PaymentProvider` (interface complète) **et** `refundProvider` (interface interne). **Commit** : `test(marketplace): add refund order unit tests` --- ## Sprint 4 : Lot S1 — Stories ### Step 7 : Story ProductDetailView connectée MSW **Fichier** : [`ProductDetailView.stories.tsx`](apps/web/src/components/marketplace/ProductDetailView.stories.tsx) — enrichir les stories existantes Ajouter une story `Error` et une story `WithManyReviews` : ```typescript /** Error state */ export const Error: Story = { name: 'Erreur', args: { product: undefined as unknown as Product, similarProducts: [], onBack: () => {}, onAddToCart: () => {}, }, }; ``` Note : La story `WithReviews` existe déjà. Pas de modifications majeures nécessaires car le composant reçoit `product` en prop avec `reviews[]`. **Commit** : `feat(storybook): enhance ProductDetailView stories with Error state` --- ## Sprint 5 : Lot D1 — Documentation ### Step 8 : Mettre à jour API_REFERENCE.md **Fichier** : [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md) — ajouter 3 sections : **Section Reviews** (après Marketplace) : ```markdown ## Reviews ### GET /marketplace/products/:id/reviews List reviews for a product (public). **Auth:** None **Query params:** `limit` (default 20), `offset` (default 0) **Example:** \`\`\`bash curl "http://localhost:8080/api/v1/marketplace/products/uuid/reviews?limit=10" \`\`\` **Response (200):** \`\`\`json { "success": true, "data": { "reviews": [ { "id": "uuid", "product_id": "uuid", "buyer_id": "uuid", "rating": 5, "comment": "Great!", "created_at": "..." } ] } } \`\`\` --- ### POST /marketplace/products/:id/reviews Create a review (buyer only, requires purchase). **Auth:** Bearer token (required) **Body:** \`\`\`json { "rating": 5, "comment": "Amazing track!" } \`\`\` **Example:** \`\`\`bash curl -X POST http://localhost:8080/api/v1/marketplace/products/uuid/reviews \ -H "Authorization: Bearer eyJ..." \ -H "Content-Type: application/json" \ -d '{"rating":5,"comment":"Amazing track!"}' \`\`\` ``` **Section Invoices** (après Orders) : ```markdown ## Invoices ### GET /marketplace/orders/:id/invoice Download invoice PDF for an order (buyer only). **Auth:** Bearer token (required) **Example:** \`\`\`bash curl -o invoice.pdf http://localhost:8080/api/v1/marketplace/orders/uuid/invoice \ -H "Authorization: Bearer eyJ..." \`\`\` **Response:** PDF binary (`Content-Type: application/pdf`) ``` **Section Refunds** (après Invoices) : ```markdown ## Refunds ### POST /marketplace/orders/:id/refund Request a refund (buyer or seller of products in order). **Auth:** Bearer token (required) **Body:** \`\`\`json { "reason": "Not as described", "details": "optional details" } \`\`\` **Example:** \`\`\`bash curl -X POST http://localhost:8080/api/v1/marketplace/orders/uuid/refund \ -H "Authorization: Bearer eyJ..." \ -H "Content-Type: application/json" \ -d '{"reason":"Not as described"}' \`\`\` **Response (200):** \`\`\`json { "success": true, "data": { "message": "Refund initiated" } } \`\`\` ``` **Commit** : `docs: add reviews, invoices, refunds to API_REFERENCE.md` --- ## Sprint 6 : Lot DOC — Finalization & Release ### Step 9 : Build validation ```bash # Backend cd veza-backend-api && go build ./... && go vet ./... && go test ./... -v # Frontend cd apps/web && npm run build ``` **Pas de commit — validation uniquement.** ### Step 10 : CHANGELOG, PROJECT_STATE, FEATURE_STATUS **Fichier** : [`CHANGELOG.md`](CHANGELOG.md) — ajouter en tête : ```markdown ## [v0.702] - 2026-02-XX ### Added - Route /marketplace/products/:id with ProductDetailPage (lazy loaded) - MSW handlers for product reviews (GET list, POST create) and invoice download - Unit tests: product reviews (6 tests), invoice generation (4 tests), refund order (5 tests) - API_REFERENCE.md: documented reviews, invoices, refunds endpoints ### Changed - ProductDetailView.stories.tsx: added Error state story ``` **Fichier** : [`docs/PROJECT_STATE.md`](docs/PROJECT_STATE.md) : - `Dernier tag` → `v0.702` - `Prochaine version` → `v0.703` - Ajouter section v0.702 dans "Ce qui est livré" **Fichier** : [`docs/FEATURE_STATUS.md`](docs/FEATURE_STATUS.md) : - Ajouter section "Livré en v0.702" - Mettre à jour la ligne Marketplace **Commit** : `docs: update CHANGELOG, PROJECT_STATE, FEATURE_STATUS for v0.702` ### Step 11 : Rétrospective + placeholder v0.703 **Fichier** : `docs/RETROSPECTIVE_V0702.md` (nouveau) **Fichier** : `docs/V0_703_RELEASE_SCOPE.md` (nouveau, placeholder) **Fichier** : [`docs/SCOPE_CONTROL.md`](docs/SCOPE_CONTROL.md) : - `Référence active` → `V0_703_RELEASE_SCOPE.md` - `Version précédente` → `V0_702_RELEASE_SCOPE.md` (archive) - Historique : `v0.702 : Phase 7 — Reviews, Factures, Remboursements — taguée` - Remplacer toutes les occurrences de `v0.702` par `v0.703` et `v0.703` par `v0.704` dans les règles **Fichier** : [`.cursorrules`](.cursorrules) : - Scope `v0.703` **Commit** : `docs: add RETROSPECTIVE_V0702, placeholder V0_703, update SCOPE_CONTROL` ### Step 12 : Archiver V0_702_RELEASE_SCOPE + tag final **Commande** : `mv docs/V0_702_RELEASE_SCOPE.md docs/archive/V0_702_RELEASE_SCOPE.md` **Commit** : `chore(docs): archive V0_702_RELEASE_SCOPE` **Tag** : `git tag v0.702` --- ## Dépendances entre steps ```mermaid graph TD S1[Step1 ProductDetailPage] --> S9[Step9 Build] S2[Step2 MSW reviews] --> S9 S3[Step3 MSW invoice] --> S9 S4[Step4 Tests reviews] --> S9 S5[Step5 Tests invoices] --> S9 S6[Step6 Tests refunds] --> S9 S7[Step7 Stories] --> S9 S8[Step8 APIRef] --> S10[Step10 Changelog] S9 --> S10 S10 --> S11[Step11 Retro] S11 --> S12[Step12 Tag] ``` Les Steps 1-8 sont indépendants et peuvent être réalisés en parallèle. --- ## Validation finale ```bash # Backend cd veza-backend-api && go build ./... && go vet ./... && go test ./... -v # Frontend cd apps/web && npm run build && npm run typecheck # Smoke test # 1. GET /marketplace/products/:id → 200 + product data # 2. POST /marketplace/products/:id/reviews → 201 (avec license) # 3. GET /marketplace/orders/:id/invoice → PDF download # 4. POST /marketplace/orders/:id/refund → 200, order.status = refunded ```