# DOP Economy

The real one: the Banco Central de la República Dominicana's reference rate for the dollar, read from its own workbook with no dependency, every business day.

DOP Economy is Kanboombo's own plug-in, running on Kanboombo's Server today. It
reads the workbook in which the Banco Central de la República Dominicana
publishes the dollar's reference rate, buy and sell, and puts each business
day's rate on its day. It is small on purpose: one source, one series, two files
of code, nothing to install.

```day title="DOP Economy on Mon 28 Sep, as the app draws it"
Mon 28 Sep
Dollar · Buy 59.27, Sell 59.56 · −0.15% · Banco Central
```

```sh download
curl -O https://kanboombo.com/developers/plugins/examples/dop-economy.zip
unzip dop-economy.zip
```

## plugin.json

```json title="plugins/dop-economy/plugin.json" source=plugins/dop-economy/plugin.json
{
  "contract": 1,
  "id": "dop-economy",
  "name": { "es": "Economía RD", "en": "DOP Economy" },
  "version": "1.1.0",
  "timezone": "America/Santo_Domingo",
  "audience": "public",
  "schedule": "30 9,17 * * 1-5",
  "backfill": "all",
  "sources": [
    {
      "name": "rate",
      "url": "https://cdn.bancentral.gov.do/documents/estadisticas/mercado-cambiario/documents/TASA_DOLAR_REFERENCIA_MC.xlsx",
      "maxBytes": 5242880
    }
  ],
  "series": [
    {
      "key": "usd-dop",
      "kind": "value",
      "cadence": "business-day",
      "dates": "observation",
      "name": { "es": "Dólar", "en": "Dollar" },
      "unit": "DOP",
      "fields": ["compra", "venta"],
      "fieldNames": {
        "compra": { "es": "Compra", "en": "Buy" },
        "venta": { "es": "Venta", "en": "Sell" }
      },
      "change": "venta",
      "snapshot": { "back": "60d", "ahead": "0d" }
    }
  ],
  "limits": { "seconds": 60, "megabytes": 128 },
  "licence": {
    "holder": "Banco Central de la República Dominicana",
    "attribution": "Banco Central",
    "terms": "https://opendatacommons.org/licenses/odbl/1-0/",
    "retain": "yes"
  }
}
```

One workbook of at most 5 MB; one value series whose change readers see on
`venta`, with `DOP` read as pesos; observation dates, so a revised rate replaces
its day and the old one is kept, and weekends show the rate in force; a 60-day
snapshot; 09:30 and 17:30 on weekdays in Santo Domingo, which keeps no daylight
time; `public`, so on your days once you install it.

## fetch.mjs and xlsx.mjs

```js title="plugins/dop-economy/fetch.mjs" source=plugins/dop-economy/fetch.mjs
#!/usr/bin/env node
// DOP Economy's fetcher (contract 1, docs/kanboombo/PLUGIN-CONTRACT.md).
//
// Reads the run context on stdin, opens the Banco Central's reference-rate
// workbook the host downloaded (source "rate"), and prints one value record
// per business day the sheet holds: the buy (compra) and sell (venta) rate.
// It prints the whole sheet every run; the runner writes only what changed,
// so a revised past rate is caught wherever it is.
//
// Exit 0 when the sheet reads as expected, even with nothing new. Exit 2 when
// the workbook is not what this plug-in knows (a renamed sheet, a moved
// column): the run then fails with that reason and nothing is written.

import { readFileSync } from "node:fs";
import { readSheet, WorkbookError } from "./xlsx.mjs";

const SHEET = "Diaria";
const MONTHS = {
  ene: 1, feb: 2, mar: 3, abr: 4, may: 5, jun: 6, jul: 7, ago: 8, sep: 9, set: 9, oct: 10, nov: 11, dic: 12,
  jan: 1, apr: 4, aug: 8, dec: 12,
};

function fail(reason) {
  process.stderr.write(`dop-economy: ${reason}\n`);
  process.exit(2);
}

/** "Año" → "ano": header cells compared without accents or case. */
function plain(value) {
  return String(value ?? "").normalize("NFD").replace(/[̀-ͯ]/g, "").trim().toLowerCase();
}

function readStdin() {
  try {
    return JSON.parse(readFileSync(0, "utf8"));
  } catch (error) {
    fail(`the run context is not JSON (${error.message})`);
  }
}

const context = readStdin();
const path = context?.sources?.rate;
if (!path) fail("the run context names no source called rate");

let rows;
try {
  rows = readSheet(readFileSync(path), SHEET);
} catch (error) {
  fail(error instanceof WorkbookError ? `the workbook changed: ${error.message}` : `cannot read the workbook: ${error.message}`);
}

const headerIndex = rows.findIndex((r) => {
  const names = r.cells.map(plain);
  return ["ano", "mes", "dia", "compra", "venta"].every((h) => names.includes(h));
});
if (headerIndex < 0) fail(`sheet ${SHEET} has no header row with Año, Mes, Día, Compra and Venta`);
const header = rows[headerIndex].cells.map(plain);
const col = Object.fromEntries(["ano", "mes", "dia", "compra", "venta"].map((h) => [h, header.indexOf(h)]));

const today = typeof context.today === "string" ? context.today : null;
const seen = new Set();
let printed = 0;
for (const { cells } of rows.slice(headerIndex + 1)) {
  const year = cells[col.ano];
  const month = MONTHS[plain(cells[col.mes]).slice(0, 3)];
  const day = cells[col.dia];
  const compra = cells[col.compra];
  const venta = cells[col.venta];
  // Notes and blank rows under the table carry no year and day; skip them.
  if (!Number.isInteger(year) || !Number.isInteger(day) || !month) continue;
  if (typeof compra !== "number" || typeof venta !== "number" || !(compra > 0) || !(venta > 0)) continue;
  const date = `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
  if (today && date > today) continue;
  if (seen.has(date)) fail(`sheet ${SHEET} lists ${date} twice`);
  seen.add(date);
  process.stdout.write(`${JSON.stringify({ kind: "value", series: "usd-dop", date, fields: { compra, venta } })}\n`);
  printed += 1;
}
if (printed === 0) fail(`sheet ${SHEET} has a header but no rates under it`);
process.stderr.write(`dop-economy: ${printed} business days read from ${SHEET}\n`);
```

It finds the header by its names, without accents or case, reads Spanish and
English months, never prints a date after today, and stops on a date listed
twice. It prints the whole sheet every run, so a revised past rate is caught
wherever it is. `xlsx.mjs` reads the workbook with nothing but Node, so the
digest covers every line that runs. Run it by hand, as the Server does:

```sh run
echo '{"today":"2026-09-30","sources":{"rate":"dop-economy/fixtures/sources/rate.xlsx"}}' | node dop-economy/fetch.mjs 2>/dev/null | tail -n 2
```

```text output
{"kind":"value","series":"usd-dop","date":"2026-09-25","fields":{"compra":59.277,"venta":59.645}}
{"kind":"value","series":"usd-dop","date":"2026-09-28","fields":{"compra":59.2669,"venta":59.5577}}
```

No rate for Thursday 24 September, a holiday, nor the weekend. When the
Banco Central renames the sheet, it stops:

```sh run exit=2
echo '{"today":"2026-09-30","sources":{"rate":"dop-economy/fixtures/lab/renamed-sheet.xlsx"}}' | node dop-economy/fetch.mjs
```

```text output
dop-economy: the workbook changed: no sheet named Diaria
```

On the Server that line is the failed run's reason: nothing is written, stored
rates stay, and readers see `Couldn't refresh` after the last rate until a run
succeeds.

## The fixtures

`fixtures/sources/rate.xlsx` holds real rows, 3 August to 28 September 2026
and March 2008, and `records.jsonl` the 60 rates they give. With no
`context.json`, `check` runs now: every rate is past, so the day does not
matter. `runs/renamed-sheet/` must fail; `lab/` holds four more workbooks for
Kanboombo's own lab.

```sh run
node kanboombo-plugin.mjs check dop-economy
```

```text output
✔ dop-economy: 60 records, 1 failure case, contract 1
  digest …
```

## Its data {#its-data}

The holder is the Banco Central de la República Dominicana, shown as
`Banco Central`; `terms` is the Open Database License (ODbL) 1.0, under which
it publishes the same rate on the Dominican open data portal; `retain` is
`yes`. Its listing links the source's page and that licence, so you read both
before you install it:

```json title="plugins/dop-economy/listing.json" source=plugins/dop-economy/listing.json
{
  "listing": 1,
  "plugin": "dop-economy",
  "publisher": {
    "domain": "kanboombo.com"
  },
  "tagline": {
    "en": "The Banco Central's dollar rate, buy and sell, on every day since 1991.",
    "es": "La tasa del dólar del Banco Central, compra y venta, en cada día desde 1991."
  },
  "description": {
    "en": "You see the US dollar's reference rate in Dominican pesos, what the Banco Central buys and sells it at, on every business day back to 1991. On a weekend or a holiday you see the rate in force and the day it was set.\n\nDOP Economy reads the Banco Central's own published file twice each business day, at 9:30 and 17:30 in Santo Domingo, and keeps every revision. A higher dollar is never shown as good or bad: you see the change, and you decide what it means.\n\nIt runs on Kanboombo's Server with no network of its own. It reads nothing about you and never sees who installed it.",
    "es": "Ves la tasa de referencia del dólar en pesos dominicanos, a cuánto lo compra y lo vende el Banco Central, en cada día laborable desde 1991. En un fin de semana o un feriado ves la tasa vigente y el día en que se fijó.\n\nEconomía RD lee el archivo que publica el propio Banco Central dos veces cada día laborable, a las 9:30 y a las 17:30 en Santo Domingo, y guarda cada revisión. Un dólar más alto nunca se muestra como bueno o malo: ves el cambio, y tú decides qué significa.\n\nCorre en el Servidor de Kanboombo, sin red propia. No lee nada sobre ti y nunca sabe quién lo instaló."
  },
  "category": "economy",
  "keywords": [
    "dollar",
    "peso",
    "DOP",
    "exchange rate",
    "tasa de cambio",
    "dólar",
    "Banco Central",
    "República Dominicana"
  ],
  "links": {
    "privacy": "https://kanboombo.com/privacy/",
    "support": {
      "url": "https://kanboombo.com/about/"
    }
  },
  "sources": [
    {
      "name": "rate",
      "title": "Tasa de cambio de referencia del dólar",
      "page": "https://www.bancentral.gov.do/a/d/2538-mercado-cambiario",
      "licence": {
        "name": "Open Database License (ODbL) 1.0",
        "url": "https://opendatacommons.org/licenses/odbl/1-0/"
      }
    }
  ],
  "examples": [
    {
      "date": "2026-09-28",
      "caption": {
        "en": "A business day",
        "es": "Un día laborable"
      }
    },
    {
      "date": "2026-09-27",
      "caption": {
        "en": "A Sunday: the rate in force",
        "es": "Un domingo: la tasa vigente"
      }
    },
    {
      "date": "2008-03-04",
      "caption": {
        "en": "March 2008",
        "es": "Marzo de 2008"
      }
    }
  ]
}
```
