Aller au contenu

Elements headless

Le même état et les mêmes règles que le Builder de Cord, sans une seule ligne d’interface : le design, c’est vous qui l’apportez.

@flouviahq/elements/headless est le cœur de Cord Elements. <CordBuilder>, les hooks React et Vue et les Web Components consomment tous ce même cœur. L’importer n’enregistre aucun élément et ne touche pas au DOM : uniquement l’état, la validation et le réseau. Utilisez-le lorsque vous voulez votre propre interface, avec n’importe quel framework ou sans framework.

npm install @flouviahq/elements
import { createCordClient, createQuoteBuilder, createFiscalForm } from '@flouviahq/elements/headless';
Export Usage
createCordClient Client HTTP du navigateur vers Cord ou vers votre proxy.
createQuoteBuilder L’état complet de la préparation d’un devis : client, lignes, taxes, totaux, envoi.
createFiscalForm L’état du formulaire fiscal du destinataire.
createStore Le store observable utilisé par les trois (get, subscribe, set).
reduceQuoteView, INITIAL_QUOTE_VIEW Le réducteur de l’état en direct d’un devis intégré.
calculateDocumentTotals, roundMoney Le moteur de totaux qu’utilise le serveur.
validateFiscalReceptor, validRfc, validSpainTaxId, validEin, fiscalText La validation fiscale et ses textes en espagnol, anglais, portugais, français et allemand.
sanitizeAppearance, appearanceToCss Le validateur de l’API Appearance.
CordError L’erreur typée, avec code et requestId.

createCordClient

const client = createCordClient({ publishableKey: 'pk_test_...' });
// ou, avec votre backend comme intermédiaire :
const viaProxy = createCordClient({ proxyUrl: '/api/cord' });

Passez l’une des deux options : publishableKey (une clé pk_, qui ne peut utiliser que les routes d’Elements) ou proxyUrl (la base de votre proxy serveur, sur le modèle de /api/v1). Avec les deux, ou avec une clé sk_, une erreur est levée.

Option Par défaut Détail
publishableKey — Clé pk_live_ ou pk_test_.
proxyUrl — Par exemple /api/cord. Les requêtes partent avec les cookies de votre site.
baseUrl https://cordhq.app Origine de Cord en mode publiable.
timeoutMs 15000 Interrompt chaque tentative.
maxRetries 2 (maximum 5) Nouvelles tentatives en cas de panne réseau, de 429 et de 5xx, en respectant Retry-After.
debug false Consigne chaque requête avec son request_id dans la console.
fetch fetch global Pour les tests.
Méthode Effet
config({ force? }) GET /elements/config : les devises, les taxes du pays, les conditions et la validité de votre organisation. La réponse est mise en cache avec son ETag et l’appel suivant envoie If-None-Match : si rien n’a changé, Cord répond 304 et la copie est utilisée. force: true la redemande en entier.
products() GET /productos : le catalogue, sans les coûts.
createQuote(input, { idempotencyKey? }) POST /cotizaciones avec une Idempotency-Key (la vôtre ou un nouvel UUID).
aiDraft(input, onItem, { signal? }) POST /elements/ai-draft : remplit les lignes par IA à partir d’un texte (texto, jusqu’à 4 000 caractères) ou d’un fichier (archivo, JPG, PNG ou PDF jusqu’à 4 Mo). Appelle onItem pour chaque ligne dès qu’elle est lue.
const config = await client.config();
console.log(config.org.moneda, config.impuestos.etiqueta, config.monedas);

createQuoteBuilder

const builder = createQuoteBuilder({
  client,
  onCreated: (quote) => console.log(quote.folio, quote.link_publico),
});

const unsubscribe = builder.subscribe((state) => render(state));

builder.setCliente({ empresa: 'Acme', email: 'purchasing@acme.com' });
const key = builder.addItem({ descripcion: 'Installation', cantidad: 1, precio_unitario: 12500 });
builder.updateItem(key, { cantidad: 2 });

const quote = await builder.submit(); // null s’il y a des erreurs ou en cas d’échec

À sa création, le builder charge de lui-même la configuration et le catalogue (status passe de loading à ready). Chaque modification recalcule les totaux avec le même moteur que le serveur, et le taux de chaque ligne provient du catalogue réel de taxes de l’organisation.

Méthode Effet
setCliente(patch) empresa, email, contacto, telefono.
setFiscal(recipient | null) Le destinataire fiscal transmis avec le devis.
setMoneda(code), setTerminos(t), setVigencia(days), setNotas(text), setPreciosIncluyenImpuesto(bool) Les conditions du devis.
addItem(item?) Ajoute une ligne et renvoie sa key. Sans tax_rate, utilise le taux par défaut.
addProduct(product, quantity?) Ajoute un produit du catalogue.
updateItem(key, patch), removeItem(key) Modifie ou supprime une ligne par sa key.
searchProducts(text, limit?) Recherche dans le catalogue chargé, par nom ou par SKU.
validate() Renvoie et enregistre les issues.
submit() Valide, crée le devis et renvoie la réponse, ou null.
draftWithAi(input, { signal? }) Remplit les lignes par IA ; chaque ligne proposée porte sugerida.
reset(), load() Vide le devis, ou recharge la configuration et le catalogue.

submit() utilise une seule Idempotency-Key par tentative d’envoi : un double clic ou une nouvelle tentative réseau ne créent jamais deux devis. La clé est renouvelée dès que vous modifiez quoi que ce soit.

L’état (builder.get()) contient :

Champ Contenu
status loading, ready, submitting, success ou error.
config, products Ce qui a été chargé depuis Cord.
cliente, fiscal, moneda, terminos, vigencia_dias, notas, precios_incluyen_impuesto Ce que la personne a saisi.
items Les lignes, chacune avec sa key.
totals subtotal, impuestos, retencionTotal, total, la ventilation par taux (porTasa), les retenciones et les lineas calculées.
issues { field, code } pour chaque problème, par exemple { field: 'items', code: 'no_items' }.
result, error Le devis créé, ou la CordError de la dernière tentative.
ai status, count et error du remplissage par IA.

Pour votre analyse, onEvent(event, data) reçoit builder.loaded, builder.item_added, builder.item_removed, builder.submitted, builder.created, builder.failed et builder.ai_drafted. Les noms restent stables d’une version à l’autre.

createFiscalForm

const fiscal = createFiscalForm({ country: 'MX' });

fiscal.subscribe((s) => {
  console.log(s.visibleErrors, s.regimenes.map((r) => r.codigo));
});

fiscal.set('tax_id', 'EKU9003173C9');
fiscal.touch('tax_id');
const recipient = fiscal.submit(); // le destinataire normalisé, ou null

builder.setFiscal(recipient);

L’état contient values, touched, validation (avec ok, value, errors, warnings et persona), visibleErrors, les regimenes et usosCfdi applicables au type de RFC, et les fields du pays. Les règles se trouvent dans Formulaire fiscal.

Avec votre framework

Le store respecte le contrat get() + subscribe(), il se branche donc sur n’importe quel système réactif :

import { useSyncExternalStore } from 'react';

function useBuilderState(builder) {
  return useSyncExternalStore(builder.subscribe, builder.get, builder.get);
}

État en direct d’un devis intégré

reduceQuoteView(state, event) transforme les événements relayés par l’iframe de <cord-quote> en un état (ready, status, folio, moneda, total, approved, rejected, paid, signedBy, lastEvent). C’est ce qui se trouve derrière la propriété state et l’événement statechange du Web Component ; voir Web Components.