npm package: @nativesquare/refurbed
Description: A refurbed component for Convex.
Latest version: 0.1.0

# @nativesquare/refurbed

A [Convex component](https://convex.dev/components) that syncs your Refurbed merchant data into local Convex tables — giving you reactive queries, multi-tenant isolation, and zero Refurbed API code in your app. Orders, offers, market offers, and reference data are all available through a simple class-based client or direct component function calls.

## Installation

```bash
npm install @nativesquare/refurbed
```

## Quick Start

### 1. Register the component

Create (or update) your `convex/convex.config.ts`:

```ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import refurbed from "@nativesquare/refurbed/convex.config.js";

const app = defineApp();
app.use(refurbed);

export default app;
```

### 2. Instantiate the client

In any Convex function file, create a `Refurbed` instance:

```ts
import { Refurbed } from "@nativesquare/refurbed";
import { components } from "./_generated/api";

const refurbed = new Refurbed(components.refurbed);
```

### 3. Create a connection and start syncing

```ts
import { Refurbed } from "@nativesquare/refurbed";
import { components } from "./_generated/api";
import { mutation } from "./_generated/server";
import { v } from "convex/values";

const refurbed = new Refurbed(components.refurbed);

export const setupRefurbed = mutation({
  args: { organizationId: v.string(), apiKey: v.string() },
  returns: v.string(),
  handler: async (ctx, args) => {
    // Creates the connection and enables sync (all entity types on by default)
    return await refurbed.createConnection(ctx, {
      organizationId: args.organizationId,
      apiKey: args.apiKey,
    });
  },
});
```

Three independent cron jobs run every 5 minutes, syncing orders (with order items), offers, and market offers automatically. Your data stays at most 5 minutes stale under normal operation.

## Connection Management

Connections link a Refurbed merchant account to your Convex app. Each connection is scoped by `organizationId` for multi-tenant isolation.

### Create a connection

```ts
const connectionId = await refurbed.createConnection(ctx, {
  organizationId: "org-acme",
  apiKey: "rfbd_live_...",
});
```

Throws if a connection already exists for the given `organizationId`. All sync toggles are enabled by default.

### List connections

```ts
import { query } from "./_generated/server";

export const listConnections = query({
  args: {},
  returns: v.any(),
  handler: async (ctx) => {
    return await refurbed.listConnections(ctx);
  },
});
```

Returns all connections with `organizationId`, sync toggles, last-sync timestamps, sync cursors, and error info. **API keys are never returned.**

### Update a connection (credential rotation)

```ts
await refurbed.updateConnection(ctx, {
  connectionId: "...",
  apiKey: "rfbd_live_new_...",
});
```

Replaces the stored API key and clears any `syncError` state, re-enabling sync on the next cron cycle.

### Delete a connection

```ts
await refurbed.deleteConnection(ctx, {
  connectionId: "...",
});
```

Removes the connection and cascade-deletes all associated synced data (orders, order items, offers, market offers) for that organization. No other organization's data is affected.

## Querying Synced Data

Synced data queries read from **local Convex tables** — they are reactive Convex queries, so subscribed clients auto-update when data changes. No API calls are made.

### Orders

```ts
import { query } from "./_generated/server";
import { v } from "convex/values";

export const listOrders = query({
  args: { organizationId: v.string() },
  returns: v.any(),
  handler: async (ctx, args) => {
    return await refurbed.listOrders(ctx, {
      organizationId: args.organizationId,
    });
  },
});
```

Filter by status and limit results:

```ts
const shipped = await refurbed.listOrders(ctx, {
  organizationId: "org-acme",
  status: "shipped",
  limit: 10,
});
```

Get a single order:

```ts
const order = await refurbed.getOrder(ctx, {
  organizationId: "org-acme",
  refurbedId: "12345",
});
```

### Order Items

```ts
const items = await refurbed.listOrderItems(ctx, {
  organizationId: "org-acme",
  orderId: "12345",
  limit: 50,
});
```

Get a single order item:

```ts
const item = await refurbed.getOrderItem(ctx, {
  organizationId: "org-acme",
  refurbedId: "67890",
});
```

### Offers

```ts
const offers = await refurbed.listOffers(ctx, {
  organizationId: "org-acme",
  status: "ACTIVE",
  limit: 20,
});

const offer = await refurbed.getOffer(ctx, {
  organizationId: "org-acme",
  refurbedId: "42",
});
```

### Market Offers

```ts
const marketOffers = await refurbed.listMarketOffers(ctx, {
  organizationId: "org-acme",
  offerId: "42",
  limit: 20,
});

const marketOffer = await refurbed.getMarketOffer(ctx, {
  organizationId: "org-acme",
  refurbedId: "42:de", // compound key: "offerId:marketCode"
});
```

## Pass-Through Reference Data

These methods call the **live Refurbed API** in real time (no local caching). They require an `ActionCtx` — use them inside Convex `action` handlers.

### Markets & Currencies

```ts
import { action } from "./_generated/server";
import { v } from "convex/values";

export const getMarkets = action({
  args: { organizationId: v.string() },
  returns: v.any(),
  handler: async (ctx, args) => {
    return await refurbed.listMarkets(ctx, {
      organizationId: args.organizationId,
    });
  },
});
```

```ts
const market = await refurbed.getMarket(ctx, {
  organizationId: "org-acme",
  marketId: "de",
});

const currencies = await refurbed.listCurrencies(ctx, {
  organizationId: "org-acme",
});

const currency = await refurbed.getCurrency(ctx, {
  organizationId: "org-acme",
  currencyId: "EUR",
});
```

### Instances, Shipping Profiles & Catalog Templates

```ts
const instance = await refurbed.getInstance(ctx, {
  organizationId: "org-acme",
  instanceId: "inst-1",
});

const profiles = await refurbed.listShippingProfiles(ctx, {
  organizationId: "org-acme",
});

const templates = await refurbed.getCatalogTemplates(ctx, {
  organizationId: "org-acme",
});
```

## Sync Control

Control which entity types are synced without redeployment.

### Update individual toggles

Updates the specified toggles on **all connections** — omitted toggles remain unchanged.

```ts
// Disable order sync on all connections, leave offers and market offers unchanged:
await refurbed.updateSettings(ctx, { syncOrdersEnabled: false });

// Enable all three toggles explicitly:
await refurbed.updateSettings(ctx, {
  syncOrdersEnabled: true,
  syncOffersEnabled: true,
  syncMarketOffersEnabled: true,
});
```

### Bulk enable / disable

```ts
await refurbed.enableAllSync(ctx);   // Enable all sync toggles on all connections
await refurbed.disableAllSync(ctx);  // Disable all sync toggles on all connections
```

### Manual sync trigger

```ts
import { action } from "./_generated/server";
import { v } from "convex/values";

export const triggerSync = action({
  args: { connectionId: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    return await refurbed.triggerSync(ctx, {
      connectionId: args.connectionId,
    });
  },
});
```

Triggers an immediate sync for all enabled entity types on the given connection. Requires `ActionCtx` because it makes HTTP calls to the Refurbed API.

## API Reference

| Method | Context | Data Source | Description |
|--------|---------|-------------|-------------|
| `createConnection(ctx, { organizationId, apiKey })` | Mutation | Local write | Create a new merchant connection |
| `listConnections(ctx)` | Query | Local read | List all connections (no API keys) |
| `updateConnection(ctx, { connectionId, apiKey })` | Mutation | Local write | Rotate API key, clear sync errors |
| `deleteConnection(ctx, { connectionId })` | Mutation | Local write | Delete connection + cascade data |
| `listOrders(ctx, { organizationId, status?, limit? })` | Query | Synced local | List synced orders with filters |
| `getOrder(ctx, { organizationId, refurbedId })` | Query | Synced local | Get single order by Refurbed ID |
| `listOrderItems(ctx, { organizationId, orderId, limit? })` | Query | Synced local | List order items for an order |
| `getOrderItem(ctx, { organizationId, refurbedId })` | Query | Synced local | Get single order item |
| `listOffers(ctx, { organizationId, status?, limit? })` | Query | Synced local | List synced offers with filters |
| `getOffer(ctx, { organizationId, refurbedId })` | Query | Synced local | Get single offer |
| `listMarketOffers(ctx, { organizationId, offerId?, limit? })` | Query | Synced local | List synced market offers |
| `getMarketOffer(ctx, { organizationId, refurbedId })` | Query | Synced local | Get single market offer |
| `triggerSync(ctx, { connectionId })` | Action | Live API | Manually trigger data sync |
| `updateSettings(ctx, { syncOrdersEnabled?, syncOffersEnabled?, syncMarketOffersEnabled? })` | Mutation | Local write | Update sync toggles |
| `enableAllSync(ctx)` | Mutation | Local write | Enable all sync toggles |
| `disableAllSync(ctx)` | Mutation | Local write | Disable all sync toggles |
| `listMarkets(ctx, { organizationId })` | Action | Live API | List all markets |
| `getMarket(ctx, { organizationId, marketId })` | Action | Live API | Get market by code |
| `listCurrencies(ctx, { organizationId })` | Action | Live API | List all currencies |
| `getCurrency(ctx, { organizationId, currencyId })` | Action | Live API | Get currency by code |
| `getInstance(ctx, { organizationId, instanceId })` | Action | Live API | Get instance by ID |
| `listShippingProfiles(ctx, { organizationId })` | Action | Live API | List shipping profiles |
| `getCatalogTemplates(ctx, { organizationId })` | Action | Live API | Get catalog templates |

## Configuration Reference

Sync toggles are per-connection boolean flags that control which entity types are synced by the cron jobs:

| Toggle | Default | Cron Interval | Entities Synced |
|--------|---------|---------------|-----------------|
| `syncOrdersEnabled` | `true` | 5 minutes | Orders + Order Items (coupled) |
| `syncOffersEnabled` | `true` | 5 minutes | Offers |
| `syncMarketOffersEnabled` | `true` | 5 minutes | Market Offers |

- Toggles can be changed at runtime via `updateSettings`, `enableAllSync`, or `disableAllSync` — no redeployment needed.
- Each entity type syncs independently — a failure in one does not prevent the others from completing.
- On auth failure (401/403), the connection is flagged with `syncError` and sync stops for that entity. Use `updateConnection` with a new API key to clear the error and resume.

## Direct Component Access

For advanced use cases, you can call component functions directly instead of using the `Refurbed` class:

```ts
import { query, mutation, action } from "./_generated/server";
import { components } from "./_generated/api";
import { v } from "convex/values";

// Query — use ctx.runQuery
export const listOrders = query({
  args: { organizationId: v.string() },
  returns: v.any(),
  handler: async (ctx, args) => {
    return await ctx.runQuery(
      components.refurbed.public.listOrders,
      args,
    );
  },
});

// Mutation — use ctx.runMutation
export const createConnection = mutation({
  args: { organizationId: v.string(), apiKey: v.string() },
  returns: v.string(),
  handler: async (ctx, args) => {
    return await ctx.runMutation(
      components.refurbed.public.createConnection,
      args,
    );
  },
});

// Action — use ctx.runAction
export const triggerSync = action({
  args: { connectionId: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    return await ctx.runAction(
      components.refurbed.public.triggerSync,
      args,
    );
  },
});
```

All public functions are available under `components.refurbed.public.*`. Use `ctx.runQuery` for queries, `ctx.runMutation` for mutations, and `ctx.runAction` for actions.

## License

[Apache-2.0](./LICENSE)