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);
}import { useCordQuoteBuilder, useCordStore } from '@flouviahq/elements/vue';
// useCordStore(store) renvoie un ShallowRef qui suit le store.const total = document.querySelector('#total');
builder.subscribe((s) => {
total.textContent = new Intl.NumberFormat(s.config?.org.locale ?? 'en', {
style: 'currency',
currency: s.moneda || 'USD',
}).format(s.totals.total);
});É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.