First-attempt commit3a5c6e184only captured the .gitignore change; the pre-commit hook silently dropped the 343 staged moves/deletes during lint-staged's "no matching task" path. This commit re-applies the intended J1 content on top ofbec75f143(which was pushed in parallel). Uses --no-verify because: - J1 only touches .md/.json/.log/.png/binaries — zero code that would benefit from lint-staged, typecheck, or vitest - The hook demonstrated it corrupts pure-rename commits in this repo - Explicitly authorized by user for this one commit Changes (343 total: 169 deletions + 174 renames): Binaries purged (~167 MB): - veza-backend-api/{server,modern-server,encrypt_oauth_tokens,seed,seed-v2} Generated reports purged: - 9 apps/web/lint_report*.json (~32 MB) - 8 apps/web/tsc_*.{log,txt} + ts_*.log (TS error snapshots) - 3 apps/web/storybook_*.json (1375+ stored errors) - apps/web/{build_errors*,build_output,final_errors}.txt - 70 veza-backend-api/coverage*.out + coverage_groups/ (~4 MB) - 3 veza-backend-api/internal/handlers/*.bak Root cleanup: - 54 audit-*.png (visual regression baselines, ~11 MB) - 9 stale MVP-era scripts (Jan 27, hardcoded v0.101): start_{iteration,mvp,recovery}.sh, test_{mvp_endpoints,protected_endpoints,user_journey}.sh, validate_v0101.sh, verify_logs_setup.sh, gen_hash.py Session docs archived (not deleted — preserved under docs/archive/): - 78 apps/web/*.md → docs/archive/frontend-sessions-2026/ - 43 veza-backend-api/*.md → docs/archive/backend-sessions-2026/ - 53 docs/{RETROSPECTIVE_V,SMOKE_TEST_V,PLAN_V0_,V0_*_RELEASE_SCOPE, AUDIT_,PLAN_ACTION_AUDIT,REMEDIATION_PROGRESS}*.md → docs/archive/v0-history/ README.md and CONTRIBUTING.md preserved in apps/web/ and veza-backend-api/. Note: The .gitignore rules preventing recurrence were already pushed in3a5c6e184and remain in place — this commit does not modify .gitignore. Refs: AUDIT_REPORT.md §11
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
- Models :
models.go(ProductReview ligne 212, Order ligne 170, License ligne 119) - Service :
service.go(CreateReview L1026, ListReviews L1071, RefundOrder L1098) - Invoice :
invoice.go(GenerateInvoice L17, buildInvoicePDF L44) - Handlers :
marketplace.go(CreateReview L314, ListReviews L350, GetOrderInvoice L791, RefundOrder L751) - Routes :
routes_marketplace.go - Frontend service :
marketplaceService.ts(createReview L235, listReviews L248, downloadInvoice L166, refundOrder L227) - ProductDetailView :
ProductDetailView.tsx - Reviews UI :
ProductDetailViewReviews.tsx - ReviewModal :
ReviewProductModal.tsx - PurchasesView :
PurchasesView.tsx - MSW marketplace :
handlers-marketplace.ts(353 lignes) - Tests webhook :
process_webhook_test.go - Stories existantes :
ProductDetailView.stories.tsx - Lazy exports :
lazyExports.ts - Routes :
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)
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
LazyProductDetaildans 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éeTestCreateReview_InvalidRating: rating=0 ou rating=6 → erreurTestCreateReview_NotPurchased: acheteur sans licence →ErrReviewNotPurchasedTestCreateReview_DuplicateReview: 2ème review du même acheteur sur même produit →ErrReviewAlreadyExistsTestListReviews_Paginated: 3 reviews, limit=2 → retourne 2 reviews,created_at DESCTestListReviews_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%PDFTestGenerateInvoice_WrongBuyer: buyer_id != order.buyer_id → erreurTestGenerateInvoice_OrderNotFound: UUID inexistant → erreurTestGenerateInvoice_WithDiscount: order avecDiscountAmountCents > 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 → statusrefunded, licencesrevoked_atsetTestRefundOrder_NotCompleted: orderpending→ErrOrderNotRefundableTestRefundOrder_NoPaymentID: order completed maisHyperswitchPaymentID=""→ErrOrderNotRefundableTestRefundOrder_Forbidden: initiatorID != buyer ni seller →ErrRefundForbiddenTestRefundOrder_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 tag→v0.702Prochaine version→v0.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 active→V0_703_RELEASE_SCOPE.mdVersion 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.702parv0.703etv0.703parv0.704dans 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