veza/docs/PLAN_V0_702_IMPLEMENTATION.md
senke 3b429e726a docs: add v0.702 scope, implementation plan, and smoke test
Define v0.702 scope (Reviews wiring, Invoices, Refunds, Product Detail route),
detailed 12-step implementation plan, and comprehensive smoke test checklist.
2026-02-23 23:52:46 +01:00

17 KiB

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


Sprint 1 : Lot W1 — Product Detail Page route

Step 1 : ProductDetailPage + lazy export + route

Fichier : apps/web/src/features/marketplace/pages/ProductDetailPage.tsx (nouveau)

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 — ajouter après LazyMarketplace :

export const LazyProductDetail = createLazyComponent(
  () =>
    import('@/features/marketplace/pages/ProductDetailPage').then((m) => ({
      default: m.ProductDetailPage,
    })),
  undefined,
  'Product Detail',
);

Fichier : index.ts — ajouter LazyProductDetail dans les exports

Fichier : LazyComponent.tsx — ajouter LazyProductDetail dans les re-exports

Fichier : routeConfig.tsx :

  • Ajouter LazyProductDetail dans les imports
  • Ajouter la route après /marketplace :
{ 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 — ajouter après le handler GET /marketplace/products/:id :

// 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 — ajouter après le handler GET /marketplace/orders/:id :

// 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 pendingErrOrderNotRefundable
  • TestRefundOrder_NoPaymentID : order completed mais HyperswitchPaymentID=""ErrOrderNotRefundable
  • TestRefundOrder_Forbidden : initiatorID != buyer ni seller → ErrRefundForbidden
  • TestRefundOrder_SellerCanRefund : seller initie le refund → succès

Mock refundProvider :

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 — enrichir les stories existantes

Ajouter une story Error et une story WithManyReviews :

/** 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 — ajouter 3 sections :

Section Reviews (après Marketplace) :

## 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) :

## 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) :

## 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

# 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 — ajouter en tête :

## [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 :

  • Dernier tagv0.702
  • Prochaine versionv0.703
  • Ajouter section v0.702 dans "Ce qui est livré"

Fichier : 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 :

  • Référence activeV0_703_RELEASE_SCOPE.md
  • Version précédenteV0_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 :

  • 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

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

# 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