Skip to content
pcreative Commerce

Write a plugin

A plugin adds things to the store without touching its code: listening to what happens, changing what is calculated, putting a screen in the panel.

A plugin adds things to the store without touching its code: listening to what happens, changing what is calculated, putting a screen in the panel.

Getting started

npx pcc-plugin nuevo my-plugin
cd my-plugin && npm install && npm run dev

npm run dev loads the plugin and loads it again every time you save a file. It loads it outside the store, on its own bus: it is there to show that it starts, that its hooks exist and that it declares the permissions they need. Real events reach it once you install it.

The three commands

CommandWhat it does
pcc-plugin nuevo <name>Creates the folder with plugin.json, src/index.js, package.json, jsconfig.json, README.md and .gitignore. The name is lowercase letters, digits and hyphens, and the folder must not exist.
pcc-plugin comprobar [dir]Validates plugin.json, checks that entry exists and reports every error at once. If it is fine, it shows what the plugin will be able to do and flags the sensitive permissions.
pcc-plugin dev [dir]Loads it in development mode and reloads it on save. Ctrl+C to quit.

Without [dir] the current folder is used. The plugin nuevo creates already has npm run dev and npm run comprobar pointing at those two.

The whole plugin

import { definirPlugin } from "@pcreative/plugin-contract"

export default definirPlugin({
  arrancar(api, { registro }) {
    api.on("pedido.creado", (pedido) => {
      registro.info(`Order ${pedido.number}: ${pedido.total.amount} €`)
    })
  },
})

Type api.on(" and the editor shows you the available events, already typed. It is the only thing you need to know to start.

Events and filters

An event tells you about something that has already happened. You cannot change the result:

api.on("cliente.registrado", (cliente) => { /* welcome them */ })

A filter lets you modify something before it is used:

api.filtro("envios.disponibles", (metodos, { carrito }) => {
  return metodos.filter((m) => m.id !== "urgente")
})

The hooks that exist today:

EventsFilters
pedido.creado, pedido.pagadoprecio.mostrado
pedido.enviado, pedido.canceladoproducto.ficha
producto.creado, producto.actualizadoenvios.disponibles
cliente.registradopagos.disponibles
inventario.agotado, carrito.abandonadocorreo.contenido
factura.emitidaimpuestos.calculados

A filter can change what is shown, but never what is charged:

  • producto.ficha changes text, but not prices, stock or variants.
  • envios.disponibles and pagos.disponibles remove, reorder or rename options, but do not change their price or surcharge.

To change a price there is precio.mostrado, which applies the same on the product page, in the cart and at checkout.

carrito.totales is still accepted but no longer applied: it changed the totals shown without changing what is charged or invoiced. If your plugin declares it, you get a warning when you check it.

Taxes and e-invoicing

These two hooks are what a tax connector uses (Stripe Tax, Avalara, each country's e-invoicing):

  • impuestos.calculados does change what is charged, which is why it needs the impuestos:escribir permission, a sensitive one. It receives the taxes of every line and shipping method ({ lines: { [id]: [{ name, code, rate, compound }] }, shipping: {…} }) plus the cart destination, and returns the same shape with the right rates. If it returns something malformed, fails or takes more than 5 seconds, the store's own rates stay: checkout is never blocked by an outside service.
  • factura.emitida arrives with every numbered invoice and credit note: issuer, customer, lines, per-rate breakdown and the chain hash. With tienda.facturas.sellar(serie, numero, { provider, status, reference, url, qr }) (permission facturas:escribir) the connector leaves its receipt, and the stamp is printed on the invoice, with its QR code.

Besides events and filters, a plugin can add UI to the panel at menu.principal, pedido.lateral, producto.lateral, ajustes.pestaña and inicio.tarjeta.

Three kinds of extension, one of them works

plugin.json says what kind of extension it is, in kind. The contract accepts three, and only one runs today:

kindWhere it would runToday
pluginIn your store, loaded by the backend from entry (JavaScript).Works. It is the only kind the loader loads. pcc-plugin nuevo creates this one.
appOn its developer's server: it has no entry, it declares a webhookUrl, and its panel contributions need a url.Not implemented. comprobar validates it, but the store does not load it and sends it nothing.
sandboxIn your store, in an isolated space with permissions granted one by one. Its entry must be a .wasm file.Not implemented. Same: it is validated and not loaded.

If you put an app or a sandbox in the plugins folder, the backend refuses it at start-up and says why: "this loader only loads plugins".

Installing it

Copy the plugin folder into plugins/ at the root of the repository (next to apps/, not inside apps/backend). In Docker the folder is /plugins inside the backend container. It appears in Panel → Extensions.

If your plugins live somewhere else, say so with PLUGINS_DIR.

To turn one off without uninstalling it:

PLUGINS_DISABLED=one,another

Handy when a plugin is causing trouble and you want to keep selling while you look at it.

Settings

If your plugin declares settings, they appear by themselves in Panel → Extensions: you do not have to write the screen. Secrets are stored apart and not returned to the browser — the panel shows "a key is stored", never the key.

A paid plugin declares a license block in plugin.json, and the store checks it before loading the plugin. It is verified against a public key (LICENSE_PUBLIC_KEY), and verified offline: a store that loses its internet cannot lose its plugins. If the licence is not valid, the plugin does not load; there is no half-working mode.

You do not need a licence while you build it: pcc-plugin dev loads in development mode, which skips the check and warns about it on every load. That mode is chosen by whoever runs the loader, never by the plugin: nothing you put in plugin.json makes a store skip the check.

The key is public on purpose: it can only check signatures, never create them.

The contract does not depend on the inside

On purpose. A plugin written today should keep working if the store changes underneath tomorrow. That is why the events talk about pedido and carrito, not the engine's tables.

See also