veza/docs/PLAN_V0_702_IMPLEMENTATION.md

511 lines
17 KiB
Markdown
Raw Normal View History

# 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<Product | null>(null);
const [similar, setSimilar] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(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 <ProductDetailViewSkeleton />;
if (error) return <ErrorDisplay error={error} onRetry={() => window.location.reload()} />;
if (!product) return <ErrorDisplay error={new Error('Product not found')} />;
return (
<ProductDetailView
product={product}
similarProducts={similar}
onBack={() => 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(<LazyProductDetail />) },
```
**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
```