Aller au contenu

La recherche est disponible uniquement en mode production. Essayez de construire puis de prévisualiser votre site pour tester la recherche localement.

Vue d'ensemble

product-service gère le catalogue de la boutique : les produits et leurs prix. Stripe est la source de vérité. Le service appelle l’API Stripe pour chaque modification, puis attend le webhook de Stripe avant de considérer la donnée comme confirmée et de l’écrire dans PostgreSQL.

Code : core/product-service/. Configuration : voir Variables d’environnement.

Interface Port par défaut Usage
REST (axum) 8093 Front admin, boutique, hôtes PHP. Routé par Traefik sur api.clairtyx.com/v1/products*.
gRPC (tonic) 50051 Hôtes Go, Rust, C, .NET. Non exposé par Traefik.
Kafka / Redpanda — Publie product.events ; consomme provider.webhooks.raw.

REST et gRPC sont deux adaptateurs minces sur le même ProductAppService : aucune logique métier n’est dupliquée, le comportement est identique par construction.

PostgreSQL ne contient que du confirmé. Une mutation n’y écrit jamais directement :

Client ──REST/gRPC──▶ ProductAppService
│ 1. appel API Stripe (synchrone)
│ 2. résultat provisoire ──▶ Valkey/Redis (TTL)
▼
réponse (confirmation_status = "created")
Stripe ──webhook──▶ webhook-ingestion-service ──▶ Kafka provider.webhooks.raw
│
product_event_applier
│ 3. écrit la ligne confirmée
│ + ligne d'outbox (même transaction)
│ 4. évince l'entrée du cache
▼
OutboxRelay ──▶ Kafka product.events
  • Un produit tout juste créé a confirmation_status: "created" tant que le webhook n’est pas arrivé (lu depuis le cache), puis "confirmed" (lu en base).
  • GET /v1/products et GET /v1/products/:id lisent uniquement PostgreSQL : un produit encore provisoire n’y apparaît qu’après son webhook.
  • Le TTL du cache est un filet de sécurité si le webhook n’arrive jamais, pas le mécanisme normal d’éviction.
  • Un produit ou un prix créé directement dans le tableau de bord Stripe est ajouté en base, déjà confirmé, à la réception de son webhook.
  • Produit : name, description, images (clés d’objets), active, plus un JSONB attributes pour tous les autres champs Stripe (metadata, tax_code, …).
  • Prix : un par Prix Stripe (label, price_amount en entier de la plus petite unité, currency, active, plus un JSONB attributes : recurring, tiers, tax_behavior, …). price_amount vaut null pour un prix sans montant unique (par paliers, montant choisi par le client).
  • Prendre en charge un nouveau champ Stripe ne demande donc pas de migration.
  • Archivage, pas suppression : un produit supprimé passe à active = false, comme chez Stripe qui interdit de supprimer un produit ayant des prix. Un prix se désactive de la même façon.
  • Prix immuables : un Prix Stripe ne se modifie pas. Changer le libellé, le montant ou la devise remplace le prix côté Stripe et réaffecte le prix par défaut du produit.
  • La commande de création { product, prices[] } est validée par un schéma JSON (schema/catalog_create.schema.json), publié sur GET /v1/products/schema.

Une photo envoyée au service est stockée dans un bucket S3 compatible (Garage) et reçoit une clé (products/<uuid>.<ext>). Stripe n’accepte que des URL : la clé lui est envoyée sous la forme <IMAGE_PUBLIC_BASE_URL>/<clé> (CDN cdn.clairtyx.com). Au retour du webhook, une URL sous cette base est reconvertie en clé.

Mécanisme Rôle
Outbox transactionnelle La ligne d’événement est écrite dans la même transaction que la ligne produit : « enregistré » et « à publier » ne peuvent pas diverger.
Relais LISTEN/NOTIFY plus interrogation toutes les 2 s, par lots de 100. Plusieurs réplicas sont sûrs (FOR UPDATE SKIP LOCKED avec bail).
Dead-letter Après 10 échecs de publication, la ligne n’est plus retentée automatiquement.
Idempotence Chaque mutation exige l’en-tête Idempotency-Key (400 sans lui). La réponse est mémorisée par couple (clé, méthode) : rejouer la requête ne rappelle pas Stripe.
Kafka Offsets validés manuellement, après traitement.

Topic product.events, types ProductCreated, ProductUpdated, ProductDeleted. Il n’y a pas d’événement de prix : toute modification d’un prix republie le produit parent complet sous ProductUpdated. La livraison est au moins une fois : dédoublonner sur event_id.

{
"event_id": "9f0c…",
"event_type": "ProductUpdated",
"occurred_at": "2026-10-13T09:00:00+00:00",
"product": {
"id": "prod_…", "stripe_product_id": "prod_Stripe…",
"name": "T-shirt", "description": "Coton",
"images": ["products/tee.png"], "active": true, "attributes": {},
"prices": [ { "id": "pri_…", "stripe_price_id": "price_…", "label": "M",
"price_amount": 1500, "currency": "eur", "active": true, "attributes": {} } ],
"created_at": "…", "updated_at": "…"
}
}

Le service ne gère ni comptes ni mots de passe : il vérifie le JWT d’accès signé par le backend (clé publique lue sur son JWKS) et applique les règles AUTH_RULES, route par route. Une route sans règle est refusée (fail closed). Le détail est dans AUTH_RULES.

PostgreSQL (produits, prix, outbox, clés d’idempotence), Valkey/Redis (cache provisoire), Kafka/Redpanda (événements), Stripe (API), un stockage S3 compatible (photos), et le backend (JWKS).

src/
├── routes/ câblage HTTP → handlers, CORS, middleware d'autorisation
├── http/handlers/api/ products, images, catalog (schéma), health, metrics
├── grpc/ adaptateur gRPC
├── config/ variables d'environnement (conf, auth, cors)
├── infrastructure/ auth (JWT + règles), idempotence, métriques, logs
└── domains/products/
├── entities/ Product, Price, identifiants
├── repositories/ PostgreSQL, cache Valkey, outbox, dead-letter
├── services/ ProductAppService (crud/), client Stripe, event applier, relais
└── events/ enveloppe product.events

Migrations SQL : core/product-service/migrations/.