npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

scopuli-convex

v0.3.0

Published

Componente Convex com a integração completa de uma loja com o Scopuli (hub de marketplaces da Amage): hub:* functions, outbox de estoque com retry e idempotência de pedidos.

Downloads

519

Readme

scopuli-convex

Componente Convex com a integração completa de uma loja com o Scopuli — o hub de marketplaces da Amage (Mercado Livre, Shopee, Mercado Pago/InfinitePay, NF-e/NFS-e).

Substitui o convex/hub.ts escrito à mão em cada loja por três linhas, e desde a v0.2 também engole o lado que a LOJA chama no Scopuli:

  • Checkout no storefront — link hospedado InfinitePay (createCheckoutLink({orderId}), idempotente, com query reativa do status) e MP Bricks completo (processPayment, public key, customers, cartões salvos, refund admin).
  • Cotação de frete no carrinhocalculateShipping({toCep, items}) com dimensões/peso lidos dos PRODUTOS (nunca do browser) e cache TTL.
  • Pedido idempotente — o Scopuli pode re-tentar createMarketplaceOrder (timeout, retry de webhook); o componente mantém um ledger por (marketplace, externalOrderId) e nunca duplica pedido nem decrementa estoque duas vezes.
  • Notificação de estoque coalescida com retry — outbox por produto onde a última mudança vence (um retry de valor antigo jamais entrega estoque stale por cima de um mais novo), com backoff 10s/30s/90s e visão operacional via hub:status.

Instalação

npm install scopuli-convex

Publicado em https://www.npmjs.com/package/scopuli-convex. Releases: npm login + npm run release (patch) ou npm run alpha (pre-release).

// convex/convex.config.ts
import { defineApp } from "convex/server";
import scopuli from "scopuli-convex/convex.config.js";

const app = defineApp();
app.use(scopuli);
export default app;
// convex/hub.ts — isto é TUDO que a loja precisa escrever
import { components } from "./_generated/api";
import { ScopuliHub, standardStoreAdapter } from "scopuli-convex";

export const hub = new ScopuliHub(components.scopuli, standardStoreAdapter());

export const {
  listPublished,
  getProduct,
  createMarketplaceOrder,
  updateOrderStatus,
  updatePaymentStatus,
  notifyStockChange,
  status,
  // Bridges internas (v0.2): dão às actions de checkout/frete acesso às
  // tabelas do app via adapter. Precisam estar re-exportadas aqui.
  getOrderForCheckout,
  getShippingProducts,
  // Diagnóstico: npx convex run hub:doctor
  doctor,
} = hub.api();

E configure no deployment da loja (npx convex env set):

| Env var | O quê | |---|---| | SCOPULI_URL | URL pública do Scopuli (ex.: https://scopuli.amageweb.com) | | SCOPULI_STORE_KEY | API key da loja no Scopuli — scopes write:inventory + write:checkout + write:shipping | | SCOPULI_STORE_ID | UUID da loja no Scopuli (paths REST) — necessário p/ checkout e frete | | SCOPULI_FROM_CEP | CEP de origem do frete (galpão da loja) — necessário p/ cotação |

Validou tudo? npx convex run hub:doctor — diagnostica componente, envs, bridges, adapter, conectividade e os scopes da key em um comando, cada falha com o fix exato. É o primeiro passo de qualquer troubleshooting. Agentes de IA: leiam o AGENTS.md (vem no pacote — node_modules/scopuli-convex/AGENTS.md).

Semântica de configuração ausente, de propósito assimétrica:

| Recurso | Sem envs | |---|---| | Outbox de estoque | no-op com warning (telemetria; o poll de 5 min do Scopuli cobre) | | Checkout / frete | ConvexError SCOPULI_NOT_CONFIGURED listando o que falta (request/response síncrono do comprador — falha silenciosa = checkout quebrado invisível) |

Avisando o Scopuli quando o estoque muda

Em qualquer mutation da loja que altere estoque, uma linha:

import { hub } from "./hub";

export const setStock = mutation({
  args: { productId: v.id("products"), estoque: v.number() },
  handler: async (ctx, args) => {
    await ctx.db.patch("products", args.productId, { estoque: args.estoque });
    await hub.queueStockNotification(ctx, {
      productId: args.productId,
      stock: args.estoque,
    });
  },
});

É transacional com a mutation (se ela falhar, nada é enfileirado), coalescido por produto e entregue com retry. Pedidos criados pelo marketplace já notificam sozinhos o estoque decrementado.

Checkout e frete no storefront (v0.2)

Exponha as funções públicas com a SUA regra de auth — uma vez:

// convex/storefront.ts
import { hub } from "./hub";

export const {
  createCheckoutLink,      // InfinitePay: pedido → URL hospedada (idempotente)
  getCheckoutLinkStatus,   // polling na página de retorno
  watchCheckoutLink,       // query REATIVA: a UI reage quando vira "paid"
  getMercadoPagoPublicKey, // inicializar o Bricks
  processPayment,          // token do Brick → pagamento (pix/cartão/boleto)
  getPaymentStatus,
  calculateShipping,       // cotação no carrinho (dimensões saem do banco)
  createCustomer, listCards, saveCard, deleteCard, // cartões salvos/1-click
} = hub.storefrontApi({
  auth: async (ctx, { fn }) => {
    // Guest checkout? Retorne true explicitamente nas funções que quiser:
    // if (fn === "calculateShipping") return true;
    return (await ctx.auth.getUserIdentity()) !== null;
  },
});

No browser fica assim:

const { checkoutUrl } = await convex.action(api.storefront.createCheckoutLink, {
  orderId: order._id,
  redirectUrl: `${origin}/checkout/retorno?order=${order._id}`,
});
window.location.href = checkoutUrl;

// Cotação no carrinho — só CEP + itens; peso/medidas vêm do servidor:
const { options } = await convex.action(api.storefront.calculateShipping, {
  toCep: cep,
  items: cart.map((i) => ({ productId: i.productId, quantity: i.qty })),
});

Garantias anti-tampering: total_amount, external_reference, itens e dimensões são derivados do PEDIDO/PRODUTOS no banco via adapter — o client só escolhe O QUE pagar/cotar, nunca QUANTO. O retorno assíncrono do pagamento continua chegando via webhook → hub:updatePaymentStatus (v0.1); o watchCheckoutLink/getCheckoutLinkStatus são o fallback de polling.

Refund é admin-only (fora do storefrontApi) — chame de uma internalAction sua: await hub.refundPayment(ctx, { paymentId, amount? }).

O contrato com o Scopuli

O backend Go chama estas funções via HTTP API (Admin Key) — os nomes re-exportados em convex/hub.ts são o contrato:

| Path | Tipo | O quê | |---|---|---| | hub:listPublished | internalQuery | Produtos publicados, paginados, normalizados PT→EN | | hub:getProduct | internalQuery | Um produto normalizado (ou null) | | hub:createMarketplaceOrder | internalMutation | Cria pedido; valida estoque ANTES (lança INSUFFICIENT_STOCK: ...); idempotente | | hub:updateOrderStatus | internalMutation | Status do pedido (shipped, delivered…) | | hub:updatePaymentStatus | internalMutation | Status de pagamento (MP/InfinitePay) | | hub:notifyStockChange | internalMutation | Compat com o hub.ts manual ({productId, newStock}) | | hub:status | internalQuery | Ops: profundidade do outbox + falhas recentes (npx convex run hub:status) |

Os shapes de resposta estão pinados em internal/adapters/convex/store.go no repo do Scopuli — mudar um lado exige mudar o outro.

Schema diferente do padrão?

O standardStoreAdapter() assume o schema canônico das lojas Amage (products em PT com índice by_situacao_and_publicado, orders + orderItems). Tudo é configurável:

standardStoreAdapter({
  tables: { products: "produtos" },
  stockField: "qtde",
  listVariations: async (ctx, doc) => [...],         // lojas com grade
  mapProduct: async (ctx, doc, mapped) => ({ ...mapped, unit: "PC" }),
  resolvePaymentOrder: async (ctx, { externalReference }) => {
    const order = await ctx.db
      .query("orders")
      .withIndex("by_external_ref", (q) => q.eq("externalRef", externalReference))
      .unique();
    return order?._id ?? null;
  },
  orderDefaults: { initialStatus: "pago" },
});

Para checkout/frete o adapter padrão também resolve: pedido de orders+orderItems (index by_orderId; customize com orderItemsIndex ou enriqueça com mapCheckoutOrder — ex. e-mail do cliente) e dimensões de peso/altura/largura/comprimento.

Para schemas muito diferentes, implemente a interface ScopuliStoreAdapter direto (5 métodos obrigatórios + getOrderForCheckout/getShippingDimensions opcionais para habilitar checkout/frete) — a idempotência e os caches continuam por conta do componente.

Testando a loja

import { convexTest } from "convex-test";
import scopuliTest from "scopuli-convex/test";
import schema from "./schema";

const t = convexTest(schema, modules);
scopuliTest.register(t);
// t.mutation(internal.hub.createMarketplaceOrder, ...) etc.

Veja example/convex/hub.test.ts — exercita o contrato exatamente como o Scopuli chama (idempotência, INSUFFICIENT_STOCK, coalescing, retry).

Desenvolvimento (deste pacote)

npm install
npm run build:codegen   # codegen do componente + tsc → dist/
npm test                # vitest (35 testes, convex-test)
npm run typecheck       # pacote + example
npm run lint

O example/ é uma loja canônica mínima usada por testes e typecheck — também serve de referência de instalação.