From 203bd82dd93fde79ac3f09de37fdf81b2ddf455c Mon Sep 17 00:00:00 2001 From: Hyko Date: Fri, 24 Apr 2026 21:48:31 -0400 Subject: [PATCH] docs(core): add README files for all core framework modules - add cron/README.md documenting the node-cron wrapper API and job registration pattern - add email/README.md documenting the Resend wrapper, env vars, and template usage - add payments/README.md documenting the payments module - add pdf/README.md documenting the pdf generation module - add themes/README.md documenting the theming system - add toast/README.md documenting the toast notification module - add users/README.md documenting the users module --- src/core/cron/README.md | 132 +++++++++++++++++ src/core/email/README.md | 155 ++++++++++++++++++++ src/core/payments/README.md | 225 +++++++++++++++++++++++++++++ src/core/pdf/README.md | 142 +++++++++++++++++++ src/core/themes/README.md | 153 ++++++++++++++++++++ src/core/toast/README.md | 145 +++++++++++++++++++ src/core/users/README.md | 275 ++++++++++++++++++++++++++++++++++++ 7 files changed, 1227 insertions(+) create mode 100644 src/core/cron/README.md create mode 100644 src/core/email/README.md create mode 100644 src/core/payments/README.md create mode 100644 src/core/pdf/README.md create mode 100644 src/core/themes/README.md create mode 100644 src/core/toast/README.md create mode 100644 src/core/users/README.md diff --git a/src/core/cron/README.md b/src/core/cron/README.md new file mode 100644 index 0000000..cd635dc --- /dev/null +++ b/src/core/cron/README.md @@ -0,0 +1,132 @@ +# Cron Framework + +Ce répertoire est un **wrapper générique autour de `node-cron`**. Il ne connaît aucune tâche spécifique — les modules et features enregistrent leurs propres jobs. Ajouter un nouveau job ne nécessite jamais de modifier `src/core/cron/`. + +--- + +## Structure + +``` +src/core/cron/ +└── index.js schedule, stop, stopAll, trigger, validate, isRunning, getJobs, getStatus +``` + +--- + +## Import + +```js +import { schedule, stop, trigger } from '@zen/core/cron'; +``` + +--- + +## API + +### `schedule(name, cronSchedule, handler, options?)` + +Enregistre un job. Si un job du même nom existe déjà, il est stoppé et remplacé. + +```js +schedule('daily-report', '0 9 * * *', async () => { + await sendReport(); +}); + +schedule('every-5min', '*/5 * * * *', async () => { + await syncData(); +}, { timezone: 'America/New_York', runOnInit: true }); +``` + +| Paramètre | Type | Description | +|-----------|------|-------------| +| `name` | `string` | Nom unique du job | +| `cronSchedule` | `string` | Expression cron (5 ou 6 champs) | +| `handler` | `async Function` | Fonction exécutée à chaque déclenchement | +| `options.timezone` | `string` | Timezone IANA (défaut : `ZEN_TIMEZONE` ou `America/Toronto`) | +| `options.runOnInit` | `boolean` | Exécuter immédiatement à l'enregistrement (défaut : `false`) | + +Retourne l'instance `node-cron` task. + +--- + +### `stop(name)` + +Stoppe et supprime un job par son nom. Retourne `true` si le job existait, `false` sinon. + +### `stopAll()` + +Stoppe et supprime tous les jobs enregistrés. + +### `trigger(name)` + +Déclenche manuellement un job sans attendre son prochain tick. Lève une `Error` si le job n'existe pas. + +```js +await trigger('daily-report'); +``` + +### `validate(expression)` + +Valide une expression cron. Retourne `boolean`. + +### `isRunning(name)` + +Vérifie si un job est actuellement enregistré. Retourne `boolean`. + +### `getJobs()` + +Retourne la liste des noms de tous les jobs enregistrés (`string[]`). + +### `getStatus()` + +Retourne les métadonnées de tous les jobs enregistrés. + +```js +{ + 'daily-report': { + schedule: '0 9 * * *', + timezone: 'America/Toronto', + registeredAt: '2026-04-24T09:00:00.000Z' + } +} +``` + +--- + +## Enregistrer un job depuis un module + +Les jobs vivent **avec leur feature ou module**, pas dans le framework. Enregistrer un job dans `initializeZen()` (`src/shared/lib/init.js`) : + +```js +// src/modules/mymodule/cron.js +import { schedule } from '@zen/core/cron'; + +export function registerCronJobs() { + schedule('mymodule-sync', '*/15 * * * *', async () => { + await syncMyModule(); + }); +} +``` + +```js +// src/shared/lib/init.js +import { registerCronJobs } from '../../modules/mymodule/cron.js'; + +registerCronJobs(); +``` + +--- + +## Comportement Hot-Reload + +Les jobs sont stockés dans `globalThis[Symbol.for('__ZEN_CRON_JOBS__')]` — un store partagé qui survit aux invalidations de cache de modules de Next.js. Un job enregistré deux fois (hot-reload) remplace silencieusement l'ancien plutôt que de créer un doublon. + +--- + +## Gestion des erreurs + +Les erreurs levées par un handler sont interceptées et loguées via `fail()` — elles ne font jamais crasher le processus. + +``` +✗ Cron daily-report: Connection timeout +``` diff --git a/src/core/email/README.md b/src/core/email/README.md new file mode 100644 index 0000000..c7c0ad8 --- /dev/null +++ b/src/core/email/README.md @@ -0,0 +1,155 @@ +# Email Framework + +Ce répertoire fournit un **wrapper autour de [Resend](https://resend.com)** pour l'envoi d'emails, ainsi qu'un composant de mise en page React Email réutilisable. Il ne connaît aucun template métier — les features créent leurs propres templates et utilisent ce module pour l'envoi. + +--- + +## Structure + +``` +src/core/email/ +├── index.js sendEmail, sendBatchEmails +└── templates/ + ├── index.js re-export + └── BaseLayout.js composant de mise en page React Email +``` + +--- + +## Import + +```js +import { sendEmail, sendBatchEmails } from '@zen/core/email'; +import { BaseLayout } from '@zen/core/email/templates'; +``` + +--- + +## Variables d'environnement + +| Variable | Obligatoire | Description | +|----------|-------------|-------------| +| `ZEN_EMAIL_RESEND_APIKEY` | Oui | Clé API Resend | +| `ZEN_EMAIL_FROM_ADDRESS` | Oui | Adresse expéditeur par défaut | +| `ZEN_EMAIL_FROM_NAME` | Non | Nom affiché de l'expéditeur | +| `ZEN_EMAIL_LOGO` | Non | URL du logo affiché dans `BaseLayout` | +| `ZEN_EMAIL_LOGO_URL` | Non | URL de destination du lien autour du logo | +| `ZEN_SUPPORT_EMAIL` | Non | Email affiché dans le footer si `supportSection` est activé | +| `ZEN_NAME` | Non | Nom de l'application (fallback du nom affiché dans `BaseLayout`) | + +--- + +## API + +### `sendEmail(email)` + +Envoie un email via Resend. Retourne `{ success, data, error }`. + +```js +const result = await sendEmail({ + to: 'user@example.com', + subject: 'Bienvenue', + html: '

Bonjour !

', +}); + +if (!result.success) { + console.error(result.error); +} +``` + +| Paramètre | Type | Description | +|-----------|------|-------------| +| `to` | `string \| string[]` | Destinataire(s) | +| `subject` | `string` | Objet de l'email | +| `html` | `string` | Corps HTML | +| `text` | `string` | Corps texte brut (optionnel) | +| `from` | `string` | Adresse expéditeur (défaut : `ZEN_EMAIL_FROM_ADDRESS`) | +| `fromName` | `string` | Nom expéditeur (défaut : `ZEN_EMAIL_FROM_NAME`) | +| `replyTo` | `string` | Adresse de réponse (optionnel) | +| `attachments` | `object[]` | Pièces jointes Resend (optionnel) | +| `tags` | `object[]` | Tags Resend (optionnel) | + +--- + +### `sendBatchEmails(emails)` + +Envoie plusieurs emails en une seule requête batch Resend. Retourne `{ success, data, error }`. + +```js +await sendBatchEmails([ + { to: 'a@example.com', subject: 'Sujet A', html: '

A

' }, + { to: 'b@example.com', subject: 'Sujet B', html: '

B

' }, +]); +``` + +Chaque objet du tableau accepte les mêmes paramètres que `sendEmail`. + +--- + +## BaseLayout + +Composant React Email (`@react-email/components`) qui fournit une structure cohérente : logo ou nom de l'app, titre optionnel, contenu, footer avec copyright et lien support. + +```jsx +import { render } from '@react-email/render'; +import { BaseLayout } from '@zen/core/email/templates'; + +const html = await render( + + Merci pour votre achat. + +); + +await sendEmail({ to: 'user@example.com', subject: 'Commande confirmée', html }); +``` + +| Prop | Type | Description | +|------|------|-------------| +| `preview` | `string` | Texte de prévisualisation (snippet email) | +| `title` | `string` | Titre affiché en haut du corps | +| `children` | `ReactNode` | Contenu de l'email | +| `companyName` | `string` | Nom affiché si pas de logo (défaut : `ZEN_NAME` ou `ZEN`) | +| `logoURL` | `string` | URL du logo (défaut : `ZEN_EMAIL_LOGO`) | +| `supportSection` | `boolean` | Afficher le lien support dans le footer (défaut : `false`) | +| `supportEmail` | `string` | Email support (défaut : `ZEN_SUPPORT_EMAIL`) | + +--- + +## Créer un template depuis une feature + +Les templates vivent **avec leur feature**, pas dans ce répertoire. + +```jsx +// src/features/auth/emails/WelcomeEmail.js +import { BaseLayout } from '@zen/core/email/templates'; +import { Text, Button } from '@react-email/components'; + +export const WelcomeEmail = ({ name, loginUrl }) => ( + + Bonjour {name}, votre compte est prêt. + + +); +``` + +```js +// src/features/auth/emails/sendWelcome.js +import { render } from '@react-email/render'; +import { sendEmail } from '@zen/core/email'; +import { WelcomeEmail } from './WelcomeEmail.js'; + +export async function sendWelcomeEmail({ to, name, loginUrl }) { + const html = await render(); + return sendEmail({ to, subject: 'Bienvenue !', html }); +} +``` + +--- + +## Gestion des erreurs + +`sendEmail` et `sendBatchEmails` ne lèvent jamais d'exception — toute erreur est capturée, loguée via `fail()`, et retournée dans `{ success: false, error }`. L'appelant vérifie `result.success`. diff --git a/src/core/payments/README.md b/src/core/payments/README.md new file mode 100644 index 0000000..1eef61e --- /dev/null +++ b/src/core/payments/README.md @@ -0,0 +1,225 @@ +# Payments Framework + +Ce répertoire fournit un **wrapper autour de [Stripe](https://stripe.com)** pour la gestion des paiements : sessions de checkout, intents, clients, remboursements et webhooks. + +--- + +## Structure + +``` +src/core/payments/ +├── index.js re-export +└── stripe.js wrapper Stripe +``` + +--- + +## Import + +```js +import { + isEnabled, + getPublishableKey, + createCheckoutSession, + createPaymentIntent, + getCheckoutSession, + getPaymentIntent, + verifyWebhookSignature, + createCustomer, + getOrCreateCustomer, + listPaymentMethods, + createRefund, +} from '@zen/core/payments'; +``` + +--- + +## Variables d'environnement + +| Variable | Obligatoire | Description | +|----------|-------------|-------------| +| `STRIPE_SECRET_KEY` | Oui | Clé secrète Stripe (côté serveur) | +| `STRIPE_PUBLISHABLE_KEY` | Oui | Clé publique Stripe (côté client) | +| `STRIPE_WEBHOOK_SECRET` | Pour les webhooks | Secret de signature des webhooks Stripe | +| `ZEN_CURRENCY` | Non | Devise par défaut pour les payment intents (défaut : `cad`) | + +--- + +## API + +### `isEnabled()` + +Retourne `true` si `STRIPE_SECRET_KEY` et `STRIPE_PUBLISHABLE_KEY` sont définis. Utiliser pour conditionner l'affichage des fonctionnalités de paiement. + +```js +if (isEnabled()) { + // afficher le bouton de paiement +} +``` + +--- + +### `getPublishableKey()` + +Retourne la clé publique Stripe, ou `null` si absente. Passer au client pour initialiser Stripe.js ou `@stripe/react-stripe-js`. + +```js +const key = getPublishableKey(); +``` + +--- + +### `createCheckoutSession(options)` + +Crée une session Stripe Checkout. Retourne la session Stripe. + +```js +const session = await createCheckoutSession({ + lineItems: [{ price: 'price_xxx', quantity: 1 }], + successUrl: 'https://example.com/success', + cancelUrl: 'https://example.com/cancel', + customerEmail: 'user@example.com', + mode: 'payment', +}); + +// Rediriger l'utilisateur vers session.url +``` + +| Paramètre | Type | Description | +|-----------|------|-------------| +| `lineItems` | `object[]` | Lignes de commande Stripe | +| `successUrl` | `string` | URL de retour après paiement réussi | +| `cancelUrl` | `string` | URL de retour après annulation | +| `customerEmail` | `string` | Email pré-rempli dans le formulaire (optionnel) | +| `clientReferenceId` | `string` | Identifiant interne pour rapprochement (optionnel) | +| `metadata` | `object` | Métadonnées Stripe (optionnel) | +| `mode` | `string` | `'payment'`, `'subscription'` ou `'setup'` (défaut : `'payment'`) | + +--- + +### `createPaymentIntent(options)` + +Crée un PaymentIntent Stripe. Retourne le PaymentIntent. + +```js +const intent = await createPaymentIntent({ + amount: 4999, // en centimes + currency: 'eur', + metadata: { orderId: '123' }, +}); +``` + +| Paramètre | Type | Description | +|-----------|------|-------------| +| `amount` | `number` | Montant en centimes | +| `currency` | `string` | Devise ISO (défaut : `ZEN_CURRENCY` ou `cad`) | +| `metadata` | `object` | Métadonnées Stripe (optionnel) | +| `automaticPaymentMethods` | `object` | Config des méthodes de paiement (défaut : `{ enabled: true }`) | + +--- + +### `getCheckoutSession(sessionId)` + +Récupère une session Checkout par son identifiant. À utiliser dans la route `successUrl` pour confirmer le paiement. + +```js +const session = await getCheckoutSession(sessionId); +``` + +--- + +### `getPaymentIntent(paymentIntentId)` + +Récupère un PaymentIntent par son identifiant. + +```js +const intent = await getPaymentIntent(paymentIntentId); +``` + +--- + +### `verifyWebhookSignature(payload, signature)` + +Vérifie la signature d'un webhook Stripe et retourne l'événement. Lève une erreur si la signature est invalide ou si `STRIPE_WEBHOOK_SECRET` est absent. + +```js +// Next.js Route Handler +export async function POST(req) { + const payload = await req.text(); + const signature = req.headers.get('stripe-signature'); + + let event; + try { + event = await verifyWebhookSignature(payload, signature); + } catch (err) { + return new Response('Signature invalide', { status: 400 }); + } + + if (event.type === 'checkout.session.completed') { + // traiter la commande + } + + return new Response('OK'); +} +``` + +Le `payload` doit être le corps brut de la requête (non parsé). + +--- + +### `createCustomer(options)` + +Crée un client Stripe. Retourne le client. + +```js +const customer = await createCustomer({ + email: 'user@example.com', + name: 'Jean Dupont', + metadata: { userId: '42' }, +}); +``` + +--- + +### `getOrCreateCustomer(email, defaultData)` + +Retourne le client Stripe existant pour cet email, ou en crée un nouveau. Utilise une clé d'idempotence dérivée de l'email pour limiter les doublons en cas d'appels concurrents. + +```js +const customer = await getOrCreateCustomer('user@example.com', { + name: 'Jean Dupont', + metadata: { userId: '42' }, +}); +``` + +--- + +### `listPaymentMethods(customerId, type)` + +Retourne la liste des méthodes de paiement d'un client. + +```js +const methods = await listPaymentMethods(customer.id, 'card'); +``` + +Le paramètre `type` est optionnel (défaut : `'card'`). + +--- + +### `createRefund(options)` + +Crée un remboursement. Retourne le remboursement Stripe. + +```js +const refund = await createRefund({ + paymentIntentId: 'pi_xxx', + amount: 1000, // partiel, en centimes (optionnel — total si absent) + reason: 'requested_by_customer', // optionnel +}); +``` + +| Paramètre | Type | Description | +|-----------|------|-------------| +| `paymentIntentId` | `string` | Identifiant du PaymentIntent à rembourser | +| `amount` | `number` | Montant en centimes (optionnel, remboursement total si absent) | +| `reason` | `string` | Raison Stripe : `duplicate`, `fraudulent`, `requested_by_customer` (optionnel) | diff --git a/src/core/pdf/README.md b/src/core/pdf/README.md new file mode 100644 index 0000000..90b8274 --- /dev/null +++ b/src/core/pdf/README.md @@ -0,0 +1,142 @@ +# PDF Framework + +Ce répertoire re-exporte les primitives de [`@react-pdf/renderer`](https://react-pdf.org) et fournit un utilitaire de nommage de fichiers. Il ne contient aucun template métier — les features créent leurs propres documents et utilisent ce module pour le rendu. + +--- + +## Structure + +``` +src/core/pdf/ +└── index.js re-exports @react-pdf/renderer + getFilename +``` + +--- + +## Import + +```js +import { + renderToBuffer, + Document, + Page, + View, + Text, + Image, + Link, + StyleSheet, + Font, + getFilename, +} from '@zen/core/pdf'; +``` + +--- + +## API + +### `renderToBuffer(element)` + +Rend un document React PDF en `Buffer`. Retourne une `Promise`. + +```js +import { renderToBuffer, Document, Page, Text } from '@zen/core/pdf'; + +const buffer = await renderToBuffer( + + + Bonjour + + +); +``` + +Utiliser ce buffer pour servir le PDF en réponse HTTP ou l'écrire sur disque. + +--- + +### `getFilename(prefix, identifier, date?)` + +Retourne un nom de fichier normalisé pour un PDF. + +```js +getFilename('invoice', '12345') +// 'invoice-12345-2024-01-15.pdf' + +getFilename('receipt', 'ORD-99', new Date('2024-06-01')) +// 'receipt-ORD-99-2024-06-01.pdf' +``` + +| Paramètre | Type | Description | +|-----------|------|-------------| +| `prefix` | `string` | Type de document (`invoice`, `receipt`, etc.) | +| `identifier` | `string` | Identifiant unique (numéro de commande, ID, etc.) | +| `date` | `Date` | Date du document (défaut : aujourd'hui) | + +--- + +### Primitives re-exportées + +Toutes les primitives de `@react-pdf/renderer` sont disponibles directement depuis `@zen/core/pdf` : + +| Export | Description | +|--------|-------------| +| `Document` | Racine d'un document PDF | +| `Page` | Page du document | +| `View` | Conteneur (équivalent `div`) | +| `Text` | Bloc de texte | +| `Image` | Image (URL ou base64) | +| `Link` | Lien hypertexte | +| `StyleSheet` | Création de styles (similaire à `StyleSheet.create` React Native) | +| `Font` | Enregistrement de polices personnalisées | + +--- + +## Créer un template depuis une feature + +Les templates vivent **avec leur feature**, pas dans ce répertoire. + +```jsx +// src/features/orders/pdf/InvoiceDocument.js +import { Document, Page, View, Text, StyleSheet } from '@zen/core/pdf'; + +const styles = StyleSheet.create({ + page: { padding: 40 }, + title: { fontSize: 20, marginBottom: 16 }, +}); + +export const InvoiceDocument = ({ order }) => ( + + + Facture #{order.number} + {order.customerName} + + +); +``` + +```js +// src/features/orders/pdf/sendInvoice.js +import { renderToBuffer, getFilename } from '@zen/core/pdf'; +import { InvoiceDocument } from './InvoiceDocument.js'; + +export async function generateInvoicePdf(order) { + const buffer = await renderToBuffer(); + const filename = getFilename('invoice', order.number); + return { buffer, filename }; +} +``` + +```js +// Next.js Route Handler +export async function GET(req, { params }) { + const order = await getOrder(params.id); + const { buffer, filename } = await generateInvoicePdf(order); + + return new Response(buffer, { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${filename}"`, + }, + }); +} +``` diff --git a/src/core/themes/README.md b/src/core/themes/README.md new file mode 100644 index 0000000..803d914 --- /dev/null +++ b/src/core/themes/README.md @@ -0,0 +1,153 @@ +# Themes + +Ce répertoire gère le thème clair/sombre de l'interface. Il expose des utilitaires client pour lire, appliquer et réagir au thème, ainsi qu'un script d'initialisation à injecter dans `` pour éviter le flash au chargement. + +--- + +## Structure + +``` +src/core/themes/ +└── index.js THEME_INIT_SCRIPT, getStoredTheme, applyTheme, getThemeIcon, ThemeWatcher, useTheme +``` + +--- + +## Import + +```js +import { + THEME_INIT_SCRIPT, + getStoredTheme, + applyTheme, + getThemeIcon, + ThemeWatcher, + useTheme, +} from '@zen/core/themes'; +``` + +Tous les exports sont marqués `'use client'`. + +--- + +## API + +### `THEME_INIT_SCRIPT` + +Script inline à injecter dans `` avant le premier rendu. Il lit `localStorage` et applique la classe `dark` sur `` immédiatement, ce qui évite le flash de thème (FOUC). + +```jsx +// app/layout.js +import { THEME_INIT_SCRIPT } from '@zen/core/themes'; + +export default function RootLayout({ children }) { + return ( + + +