@datarails-ai/dr-cli 0.0.1-locked → 1.6.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +608 -0
- package/dist/chunk-DWMCBGQH.js +242 -0
- package/dist/cli.js +64468 -0
- package/dist/kb-6UWI7Q32.js +21 -0
- package/dist/scopes-map.json +4412 -0
- package/dist/skills/connector-sandbox/SKILL.md +95 -0
- package/dist/skills/datarails-connect-workato/SKILL.md +140 -0
- package/dist/skills/datarails-connectors-onboard-connector/SKILL.md +813 -0
- package/dist/skills/dr-cli/SKILL.md +659 -0
- package/dist/skills/dr-cli/reference/admin.md +796 -0
- package/dist/skills/dr-cli/reference/budget-builder.md +151 -0
- package/dist/skills/dr-cli/reference/calc-vals.md +73 -0
- package/dist/skills/dr-cli/reference/calcmodels.md +76 -0
- package/dist/skills/dr-cli/reference/cash.md +61 -0
- package/dist/skills/dr-cli/reference/dashboards-widgets.md +85 -0
- package/dist/skills/dr-cli/reference/datamappers.md +150 -0
- package/dist/skills/dr-cli/reference/desk.md +472 -0
- package/dist/skills/dr-cli/reference/filebox-documents.md +373 -0
- package/dist/skills/dr-cli/reference/forecast.md +51 -0
- package/dist/skills/dr-cli/reference/integrations.md +57 -0
- package/dist/skills/dr-cli/reference/lookups.md +168 -0
- package/dist/skills/dr-cli/reference/metrics.md +512 -0
- package/dist/skills/dr-cli/reference/multi-product.md +180 -0
- package/dist/skills/dr-cli/reference/reconciliation-mec.md +76 -0
- package/dist/skills/dr-cli/reference/rh-config.md +84 -0
- package/dist/skills/dr-cli/reference/safety.md +143 -0
- package/dist/skills/dr-cli/reference/spend-control.md +206 -0
- package/dist/skills/dr-cli/reference/templates.md +163 -0
- package/dist/skills/dr-cli/reference/workflows.md +137 -0
- package/dist/skills/dr-error-enrichment/SKILL.md +80 -0
- package/dist/skills/dr-error-enrichment/reference/core.md +257 -0
- package/dist/skills/dr-error-enrichment/reference/groundcover.md +84 -0
- package/dist/skills/dr-error-enrichment/reference/jira-bugs.md +219 -0
- package/dist/skills/dr-error-enrichment/reference/safety-backup.md +122 -0
- package/dist/skills/dr-error-enrichment-author/SKILL.md +219 -0
- package/dist/skills/dr-error-enrichment-improve/SKILL.md +218 -0
- package/dist/skills/dr-error-enrichment-maintain/SKILL.md +237 -0
- package/dist/skills/dr-error-enrichment-reconcile/SKILL.md +167 -0
- package/dist/skills/dr-filebox-api-setup/SKILL.md +170 -0
- package/dist/skills/dr-switch-user/SKILL.md +395 -0
- package/dist/skills/dr-validate/SKILL.md +392 -0
- package/dist/skills/dr-validate/report-template.html +950 -0
- package/dist/skills/fx-conversion-matrix/SKILL.md +763 -0
- package/dist/skills/fx-conversion-matrix/scripts/_args.mjs +52 -0
- package/dist/skills/fx-conversion-matrix/scripts/_xlsx.mjs +241 -0
- package/dist/skills/fx-conversion-matrix/scripts/build-matrix.mjs +366 -0
- package/dist/skills/fx-conversion-matrix/scripts/gen-mappers.mjs +171 -0
- package/dist/skills/fx-conversion-matrix/scripts/rates-to-xlsx.mjs +172 -0
- package/dist/skills/fx-onboard/SKILL.md +325 -0
- package/dist/skills/mec-table-validation/SKILL.md +383 -0
- package/dist/skills/plugins.json +24 -0
- package/dist/skills/validate-workato-migration/SKILL.md +167 -0
- package/dist/skills/validate-workato-migration/reference/gotchas.md +16 -0
- package/dist/skills/validate-workato-migration/reference/staging-orgs.md +26 -0
- package/dist/skills/validate-workato-migration/scripts/compare_files.py +255 -0
- package/dist/zendeskKb-JPSHVOPH.js +243 -0
- package/package.json +56 -9
- package/scripts/postinstall.mjs +91 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Emit one `dr datamappers create from-file` body per target currency.
|
|
4
|
+
*
|
|
5
|
+
* Each mapper reads one `To <CCY>` tab of the conversion filebox and writes one
|
|
6
|
+
* column per rate type onto the FX table. Coordinates come from the layout
|
|
7
|
+
* sidecar `build-matrix.mjs` wrote, so the two scripts cannot disagree about a
|
|
8
|
+
* cell -- which matters once rate types shift the columns (one type puts the
|
|
9
|
+
* cross-rate in C, two put them in C and E, pairs mode puts the first in B).
|
|
10
|
+
*
|
|
11
|
+
* Two things the body cannot express, and the caller must supply:
|
|
12
|
+
*
|
|
13
|
+
* 1. `--field "<column>:Float"` on the create call, one per mapped column. A
|
|
14
|
+
* `map_to` naming a column the template lacks BOOTSTRAPS that field --
|
|
15
|
+
* that is how these columns come into being, there is no add-field API.
|
|
16
|
+
* But the bootstrapped type is inferred from the sample values and is
|
|
17
|
+
* often Text, and a Text rate column breaks every downstream calculation
|
|
18
|
+
* without complaining. The printed command lines always carry the
|
|
19
|
+
* overrides.
|
|
20
|
+
*
|
|
21
|
+
* 2. `document_version_id` is required by `from-file` and is NOT present in
|
|
22
|
+
* `dr datamappers get` output. Read it from the upload response or
|
|
23
|
+
* `dr filebox versions <filebox_id>`.
|
|
24
|
+
*
|
|
25
|
+
* The currency column maps to whatever `currency_field` the layout sidecar carries,
|
|
26
|
+
* which build-matrix defaults to the UDM `Posting Currency`. The period calc fields
|
|
27
|
+
* below write `Reporting Month`, which UDM requires to be the last day of the month
|
|
28
|
+
* at 00:00:00 UTC -- `[System_Date]` on a month-tagged filebox already is.
|
|
29
|
+
*
|
|
30
|
+
* The body deliberately carries NO `template_id`. In power mode that key names
|
|
31
|
+
* the SOURCE template a mapper was learned on, not the target (the target is
|
|
32
|
+
* the positional table id), and the CLI resolves every `map_to` type from it
|
|
33
|
+
* and THROWS on any name the source lacks -- before --field overrides are
|
|
34
|
+
* merged, so --field cannot rescue it. The rate columns do not exist anywhere
|
|
35
|
+
* yet, by design, so carrying template_id would fail every create. Omitting it
|
|
36
|
+
* opts into server-side inference, and --field pins the type.
|
|
37
|
+
*
|
|
38
|
+
* Usage:
|
|
39
|
+
* node gen-mappers.mjs --layout-file matrix.xlsx.layout.json \
|
|
40
|
+
* --filebox 125890 --version 843990 --template 2760 \
|
|
41
|
+
* [--targets EUR,GBP] [--out-dir ./mappers] [--name-prefix fx_conversion_rates] \
|
|
42
|
+
* [--ordinal-start 1] [--server DEV]
|
|
43
|
+
*/
|
|
44
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
45
|
+
import { join } from 'node:path';
|
|
46
|
+
|
|
47
|
+
import { die, parseArgs, splitCodes, str } from './_args.mjs';
|
|
48
|
+
|
|
49
|
+
const args = parseArgs(process.argv.slice(2));
|
|
50
|
+
const layoutFile = str(args, 'layout-file');
|
|
51
|
+
const filebox = Number(args.filebox);
|
|
52
|
+
const version = Number(args.version);
|
|
53
|
+
const template = Number(args.template);
|
|
54
|
+
const outDir = str(args, 'out-dir') ?? './mappers';
|
|
55
|
+
const namePrefix = str(args, 'name-prefix') ?? 'fx_conversion_rates';
|
|
56
|
+
const ordinalStart = args['ordinal-start'] === undefined ? 1 : Number(args['ordinal-start']);
|
|
57
|
+
const serverFlag = str(args, 'server') ?? '<S>';
|
|
58
|
+
|
|
59
|
+
if (layoutFile === undefined) {
|
|
60
|
+
die('--layout-file <matrix.xlsx.layout.json> is required (written by build-matrix.mjs)');
|
|
61
|
+
}
|
|
62
|
+
for (const [flag, value] of [
|
|
63
|
+
['--filebox', filebox],
|
|
64
|
+
['--version', version],
|
|
65
|
+
['--template', template],
|
|
66
|
+
]) {
|
|
67
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
68
|
+
die(`${flag} <id> is required and must be a positive integer`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let layout;
|
|
73
|
+
try {
|
|
74
|
+
layout = JSON.parse(await readFile(layoutFile, 'utf8'));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
die(`could not read ${layoutFile}: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(layout?.sheets) || layout.sheets.length === 0) {
|
|
79
|
+
die(`${layoutFile} carries no sheets -- was it written by build-matrix.mjs?`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (layout.layout === 'lut') {
|
|
83
|
+
die(
|
|
84
|
+
'this layout was built for wiring B (filebox-sourced lookups), which needs no\n' +
|
|
85
|
+
' datamappers. Use `dr lut create --payload-file` per tab instead, or rebuild\n' +
|
|
86
|
+
' with --layout matrix if you meant wiring A.',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const wanted = str(args, 'targets');
|
|
91
|
+
const only = wanted === undefined ? undefined : new Set(splitCodes(wanted));
|
|
92
|
+
|
|
93
|
+
const sheets = layout.sheets.filter((s) => only === undefined || only.has(s.target));
|
|
94
|
+
if (sheets.length === 0) die('--targets matched none of the sheets in the layout file');
|
|
95
|
+
|
|
96
|
+
/** One `config.header` entry. Every field but the three varying ones is fixed. */
|
|
97
|
+
const headerEntry = (coordinate, value, mapTo) => ({
|
|
98
|
+
role: 'header',
|
|
99
|
+
coordinate,
|
|
100
|
+
value,
|
|
101
|
+
groups: [],
|
|
102
|
+
map_to: mapTo,
|
|
103
|
+
match_type: 'value',
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function mapperBody(sheetSpec, ordinal) {
|
|
107
|
+
const { sheet, columns, target, currency_coordinate: currencyCoord } = sheetSpec;
|
|
108
|
+
return {
|
|
109
|
+
name: `${namePrefix}_${template}_${target.toLowerCase()}`,
|
|
110
|
+
sheet_name: sheet,
|
|
111
|
+
document_version_id: version,
|
|
112
|
+
selected_document_ids: [filebox],
|
|
113
|
+
ordinal,
|
|
114
|
+
config: {
|
|
115
|
+
sheets_filter: { values: [sheet], is_excluded: false },
|
|
116
|
+
preview_sheet_name: sheet,
|
|
117
|
+
header: [
|
|
118
|
+
// The sidecar names the currency field; the fallback only defends a
|
|
119
|
+
// hand-edited layout file.
|
|
120
|
+
headerEntry(currencyCoord, 'Currency', layout.currency_field ?? 'Posting Currency'),
|
|
121
|
+
...columns.map((c) => headerEntry(c.coordinate, c.name, c.name)),
|
|
122
|
+
],
|
|
123
|
+
footer: [],
|
|
124
|
+
dimension: [],
|
|
125
|
+
attributes: [],
|
|
126
|
+
table_id: [],
|
|
127
|
+
value_field_name: 'value',
|
|
128
|
+
// Both period columns come off the filebox version's own date tag, so
|
|
129
|
+
// rolling the file forward re-stamps the rows without a formula edit.
|
|
130
|
+
calculated_fields: [
|
|
131
|
+
{ name: 'Posting Date', formula: ' [System_Date] ', role: 'calculated_fields' },
|
|
132
|
+
{ name: 'Reporting Month', formula: ' [System_Date] ', role: 'calculated_fields' },
|
|
133
|
+
],
|
|
134
|
+
fx_conversions: [],
|
|
135
|
+
ignore_zeros: true,
|
|
136
|
+
improved_etl: null,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await mkdir(outDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
const commands = [];
|
|
144
|
+
for (const [i, spec] of sheets.entries()) {
|
|
145
|
+
const body = mapperBody(spec, ordinalStart + i);
|
|
146
|
+
const file = join(outDir, `${spec.target.toLowerCase()}.json`);
|
|
147
|
+
await writeFile(file, `${JSON.stringify(body, null, 2)}\n`, 'utf8');
|
|
148
|
+
console.log(
|
|
149
|
+
`wrote ${file} (sheet "${spec.sheet}" -> ` +
|
|
150
|
+
`${spec.columns.map((c) => `${c.coordinate} "${c.name}"`).join(', ')})`,
|
|
151
|
+
);
|
|
152
|
+
// The target table is a POSITIONAL argument, not a flag. Every mapped column
|
|
153
|
+
// gets an explicit Float, or the server infers a type from the samples.
|
|
154
|
+
const overrides = spec.columns
|
|
155
|
+
.map((c) => `--field ${JSON.stringify(`${c.name}:Float`)}`)
|
|
156
|
+
.join(' ');
|
|
157
|
+
commands.push(
|
|
158
|
+
`dr datamappers create from-file ${template} --file ${file} ${overrides} ` +
|
|
159
|
+
`--server ${serverFlag} --json`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log('\nRun these in order (each is a prod-tier write -- confirm first):\n');
|
|
164
|
+
for (const c of commands) console.log(` ${c}`);
|
|
165
|
+
|
|
166
|
+
const allColumns = [...new Set(sheets.flatMap((s) => s.columns.map((c) => c.name)))];
|
|
167
|
+
console.log(
|
|
168
|
+
`\nThen confirm all ${allColumns.length} column(s) landed as Float, not Text --\n` +
|
|
169
|
+
`on the FX table AND on every table a lookup carries them into:\n` +
|
|
170
|
+
` dr templates get ${template} --server ${serverFlag} --json\n`,
|
|
171
|
+
);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Turn a triangulated rate feed into a presentable .xlsx.
|
|
4
|
+
*
|
|
5
|
+
* Reads the shape a triangulating feed produces -- one row per ordered pair per
|
|
6
|
+
* day, cross-rated at the source rather than in a workbook:
|
|
7
|
+
*
|
|
8
|
+
* from_currency, to_currency, rate, timestamp
|
|
9
|
+
*
|
|
10
|
+
* A CSV in that shape is correct but is not what anyone hands a customer. This
|
|
11
|
+
* pivots it into a square matrix for one date and keeps the daily detail on a
|
|
12
|
+
* second tab.
|
|
13
|
+
*
|
|
14
|
+
* Column names are matched case-insensitively and fall back to position, so a
|
|
15
|
+
* feed that calls them `source`/`target` still works.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node rates-to-xlsx.mjs --in rates.csv --out matrix.xlsx [--date 2026-08-31]
|
|
19
|
+
*
|
|
20
|
+
* `--date` picks the matrix tab's date; the default is the latest in the file.
|
|
21
|
+
* No dependencies -- see `_xlsx.mjs`.
|
|
22
|
+
*/
|
|
23
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
24
|
+
|
|
25
|
+
import { die, parseArgs, str } from './_args.mjs';
|
|
26
|
+
import { buildWorkbook, colName, numberCell, sheetXml, textCell } from './_xlsx.mjs';
|
|
27
|
+
|
|
28
|
+
const args = parseArgs(process.argv.slice(2));
|
|
29
|
+
const inPath = str(args, 'in');
|
|
30
|
+
const outPath = str(args, 'out') ?? 'rates.xlsx';
|
|
31
|
+
const wantDate = str(args, 'date');
|
|
32
|
+
|
|
33
|
+
if (inPath === undefined) {
|
|
34
|
+
die('--in <rates.csv> is required (the file a triangulating data-query produced)');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Split one CSV line, honouring double-quoted fields. */
|
|
38
|
+
function splitCsvLine(line) {
|
|
39
|
+
const out = [];
|
|
40
|
+
let cur = '';
|
|
41
|
+
let quoted = false;
|
|
42
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
43
|
+
const ch = line[i];
|
|
44
|
+
if (quoted) {
|
|
45
|
+
if (ch === '"' && line[i + 1] === '"') {
|
|
46
|
+
cur += '"';
|
|
47
|
+
i += 1;
|
|
48
|
+
} else if (ch === '"') {
|
|
49
|
+
quoted = false;
|
|
50
|
+
} else {
|
|
51
|
+
cur += ch;
|
|
52
|
+
}
|
|
53
|
+
} else if (ch === '"') {
|
|
54
|
+
quoted = true;
|
|
55
|
+
} else if (ch === ',') {
|
|
56
|
+
out.push(cur);
|
|
57
|
+
cur = '';
|
|
58
|
+
} else {
|
|
59
|
+
cur += ch;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
out.push(cur);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const raw = (await readFile(inPath, 'utf8')).trim();
|
|
67
|
+
if (raw === '') die(`${inPath} is empty`);
|
|
68
|
+
const lines = raw.split(/\r?\n/);
|
|
69
|
+
const header = splitCsvLine(lines[0]).map((h) => h.trim().toLowerCase());
|
|
70
|
+
|
|
71
|
+
/** Locate a column by any of its known spellings, else by position. */
|
|
72
|
+
const pick = (names, fallbackIndex) => {
|
|
73
|
+
for (const n of names) {
|
|
74
|
+
const i = header.indexOf(n);
|
|
75
|
+
if (i !== -1) return i;
|
|
76
|
+
}
|
|
77
|
+
return fallbackIndex;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const iFrom = pick(['from_currency', 'from', 'source', 'source_currency'], 0);
|
|
81
|
+
const iTo = pick(['to_currency', 'to', 'target', 'target_currency'], 1);
|
|
82
|
+
const iRate = pick(['rate', 'mid', 'value'], 2);
|
|
83
|
+
const iDate = pick(['timestamp', 'date', 'reporting_month', 'posting_date'], 3);
|
|
84
|
+
|
|
85
|
+
const records = [];
|
|
86
|
+
for (const line of lines.slice(1)) {
|
|
87
|
+
if (line.trim() === '') continue;
|
|
88
|
+
const f = splitCsvLine(line);
|
|
89
|
+
const rate = Number(f[iRate]);
|
|
90
|
+
if (!Number.isFinite(rate)) continue;
|
|
91
|
+
records.push({
|
|
92
|
+
from: (f[iFrom] ?? '').trim(),
|
|
93
|
+
to: (f[iTo] ?? '').trim(),
|
|
94
|
+
rate,
|
|
95
|
+
// Date only -- a triangulated feed is daily, and the matrix is per day.
|
|
96
|
+
date: (f[iDate] ?? '').trim().slice(0, 10),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (records.length === 0) die(`${inPath} carried no usable rate rows`);
|
|
101
|
+
|
|
102
|
+
const dates = [...new Set(records.map((r) => r.date))].sort();
|
|
103
|
+
const latest = dates.at(-1);
|
|
104
|
+
const matrixDate = wantDate ?? latest;
|
|
105
|
+
if (!dates.includes(matrixDate)) {
|
|
106
|
+
die(`--date ${matrixDate} is not in the file (it holds ${dates[0]} … ${latest})`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const codes = [...new Set(records.map((r) => r.from))].sort();
|
|
110
|
+
const onDate = new Map(
|
|
111
|
+
records.filter((r) => r.date === matrixDate).map((r) => [`${r.from}>${r.to}`, r.rate]),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// --- Sheet 1: the square matrix for one date -------------------------------
|
|
115
|
+
const matrix = new Map();
|
|
116
|
+
matrix.set(1, [textCell('A1', `FX Conversion Matrix — ${matrixDate}`)]);
|
|
117
|
+
matrix.set(2, [
|
|
118
|
+
textCell('A2', 'Read as: 1 unit of the ROW currency buys this many of the COLUMN currency'),
|
|
119
|
+
]);
|
|
120
|
+
matrix.set(4, [
|
|
121
|
+
textCell('A4', 'from \\ to'),
|
|
122
|
+
...codes.map((c, i) => textCell(`${colName(2 + i)}4`, c)),
|
|
123
|
+
]);
|
|
124
|
+
codes.forEach((from, i) => {
|
|
125
|
+
const r = 5 + i;
|
|
126
|
+
const cells = [textCell(`A${r}`, from)];
|
|
127
|
+
codes.forEach((to, j) => {
|
|
128
|
+
const v = onDate.get(`${from}>${to}`);
|
|
129
|
+
if (v !== undefined) cells.push(numberCell(`${colName(2 + j)}${r}`, v));
|
|
130
|
+
});
|
|
131
|
+
matrix.set(r, cells);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// --- Sheet 2: every pair, every day ----------------------------------------
|
|
135
|
+
const daily = new Map();
|
|
136
|
+
daily.set(1, [
|
|
137
|
+
textCell('A1', 'from_currency'),
|
|
138
|
+
textCell('B1', 'to_currency'),
|
|
139
|
+
textCell('C1', 'rate'),
|
|
140
|
+
textCell('D1', 'date'),
|
|
141
|
+
]);
|
|
142
|
+
records.forEach((r, i) => {
|
|
143
|
+
const n = i + 2;
|
|
144
|
+
daily.set(n, [
|
|
145
|
+
textCell(`A${n}`, r.from),
|
|
146
|
+
textCell(`B${n}`, r.to),
|
|
147
|
+
numberCell(`C${n}`, r.rate),
|
|
148
|
+
textCell(`D${n}`, r.date),
|
|
149
|
+
]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Literal data only -- nothing to recalculate, so no fullCalcOnLoad.
|
|
153
|
+
await writeFile(
|
|
154
|
+
outPath,
|
|
155
|
+
buildWorkbook([
|
|
156
|
+
{ name: 'Conversion Matrix', xml: sheetXml(matrix) },
|
|
157
|
+
{ name: 'Daily Rates', xml: sheetXml(daily) },
|
|
158
|
+
]),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const totalPairs = codes.length ** 2;
|
|
162
|
+
const missing = totalPairs - onDate.size;
|
|
163
|
+
console.log(
|
|
164
|
+
`wrote ${outPath} — ${codes.length}×${codes.length} matrix for ${matrixDate}, ` +
|
|
165
|
+
`${records.length} daily rows across ${dates.length} date(s)`,
|
|
166
|
+
);
|
|
167
|
+
if (missing > 0) {
|
|
168
|
+
console.log(
|
|
169
|
+
`note: ${missing} of ${totalPairs} pairs are absent on ${matrixDate} — ` +
|
|
170
|
+
`those cells are blank rather than guessed`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fx-onboard
|
|
3
|
+
description: Use when enabling the FX currency selector for a Datarails customer — turning an existing org's Monetary fields, DR.GET functions and dashboard widgets currency-aware so users can switch a dashboard or workbook between currencies. Triggers - "enable FX for <customer>", "set up the currency selector", "make this org currency-aware", "roll out FX conversion to <org>", "turn on multi-currency for this tenant", "FX onboarding", "which tables should be currency aware", "why doesn't the currency dropdown show anything", "the currency selector says N/A", "tag the posting and reporting amounts", "set the org default currency", "roll back the FX conversion". Maps the org, proposes a plan, and only writes after the implementor approves. Run dr-cli:fx-conversion-matrix FIRST - the selector swaps between per-currency reporting-amount columns that must already exist, and that skill is what computes them. Installing or updating the CLI is dr-cli:install; a misbehaving CLI is dr-cli:doctor.
|
|
4
|
+
allowed-tools: Bash, Read, Write
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# FX onboarding — enable the currency selector for one org
|
|
8
|
+
|
|
9
|
+
Takes an existing customer from "no FX" to a working currency selector: Monetary
|
|
10
|
+
fields tagged with their FX roles, DR.GET functions and dashboard widgets
|
|
11
|
+
currency-aware, and an org default currency set.
|
|
12
|
+
|
|
13
|
+
This skill teaches the FX flow, not the CLI. The `dr-cli` skill is the command
|
|
14
|
+
authority for anything outside `dr fx`, and its **Auth** section covers login and
|
|
15
|
+
identities. It ships both in the Claude Code plugin and in the npm package, so it is
|
|
16
|
+
loaded wherever this skill is.
|
|
17
|
+
|
|
18
|
+
**Division of authority:** the dry run is the authority on mechanics — what will
|
|
19
|
+
change, in which currency, and on which evidence. This file is the authority on
|
|
20
|
+
*order*, on *when to stop and ask a human*, and on the hazards the commands cannot
|
|
21
|
+
see. Where the two disagree, believe the command output and fix this file.
|
|
22
|
+
|
|
23
|
+
## Before this skill: do the reporting-amount columns exist?
|
|
24
|
+
|
|
25
|
+
**Run `dr-cli:fx-conversion-matrix` first.** The selector does not convert anything.
|
|
26
|
+
It **swaps columns**: when a viewer picks GBP, `FxReportingFieldResolver` looks for a
|
|
27
|
+
sibling field on the same template with `fx_role=reporting_amount` and
|
|
28
|
+
`currency_code='GBP'`, and queries that column instead. Its own docstring is blunt
|
|
29
|
+
about it — *"No exchange-rate arithmetic happens here. The converted amounts already
|
|
30
|
+
exist as sibling columns on the same template."*
|
|
31
|
+
|
|
32
|
+
So the converted amounts must already be in the table, one column per currency, before
|
|
33
|
+
this skill can tag them. If no sibling column exists for a currency, the resolver
|
|
34
|
+
returns nothing and that currency simply never appears in the selector — visible
|
|
35
|
+
absence, not wrong numbers. The columns are what this skill makes *selectable*; they
|
|
36
|
+
are not what it creates.
|
|
37
|
+
|
|
38
|
+
Getting those columns computed — posting amount times a rate, per currency — is
|
|
39
|
+
`dr-cli:fx-conversion-matrix`. Land the rates there first, compute the reporting
|
|
40
|
+
amounts, then come back here to tag them.
|
|
41
|
+
|
|
42
|
+
> Not to be confused with the older `CurrencyConversionService` (reached through
|
|
43
|
+
> `CoreEngine.db.table_api_djm`), which DOES multiply by a rate, read from one
|
|
44
|
+
> server-wide table named by `fx_rate_table_id` on the `datarails.com` org. That path
|
|
45
|
+
> is separate from the selector and this skill does not use it. Worth knowing because
|
|
46
|
+
> its failure mode is nastier: its SQL defaults an unresolved rate to 1 (DR-52685), so
|
|
47
|
+
> a stale rate table silently returns unconverted numbers.
|
|
48
|
+
|
|
49
|
+
## What this actually is
|
|
50
|
+
|
|
51
|
+
Not a toggle. On an existing customer the amount columns are `Float`, so this
|
|
52
|
+
**retypes customer columns to `Monetary`** and triggers a rescan. The org sees new
|
|
53
|
+
behaviour in dashboards and Excel afterwards. Treat it as a change to a production
|
|
54
|
+
tenant, because it is one.
|
|
55
|
+
|
|
56
|
+
The hard part is not running the commands. It is answering **"what currency is this
|
|
57
|
+
column in?"** — the question CS raised in the FX focus group, because for most
|
|
58
|
+
customers `amount` carries no clue. This skill's job is to gather the evidence, and
|
|
59
|
+
to **stop and ask a human when the evidence is not there.** Never guess a currency.
|
|
60
|
+
|
|
61
|
+
## Order is not optional
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
flags → posting → reporting → default currency → widgets + functions → verify
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Currency conversion needs a value field that is `Monetary` **and** carries an
|
|
68
|
+
`fx_role`. The server answers a conversion request on an untagged field with a
|
|
69
|
+
**400, not a skip**, so running the converters first makes every widget report
|
|
70
|
+
`needs-fx-role`. Tag fields first, always.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Step 0 — Preflight
|
|
75
|
+
|
|
76
|
+
Do not reimplement the CLI install. **With the `dr-cli` plugin**, its hook installs the
|
|
77
|
+
pinned version before this skill runs; if `dr` is missing or the wrong version, route
|
|
78
|
+
to `/dr-cli:install`, and if it is installed but misbehaving, to `/dr-cli:doctor`.
|
|
79
|
+
**Without the plugin** (this skill came from the npm package),
|
|
80
|
+
`dr` is already installed, because that install is what delivered this skill. Update
|
|
81
|
+
it with `dr update`, and use the `dr-cli` skill's troubleshooting for a misbehaving CLI.
|
|
82
|
+
|
|
83
|
+
Four things this skill does check, because each one silently wastes the whole session:
|
|
84
|
+
|
|
85
|
+
1. **`dr fx` exists.** `dr fx --help` must list `set-posting`, `set-reporting`,
|
|
86
|
+
`default-currency`, `convert-functions`, `convert-widgets`. If it does not, the CLI
|
|
87
|
+
predates FX — `/dr-cli:install`, or `dr update` without the plugin.
|
|
88
|
+
2. **You are on the right tenant, as the right identity.** Run
|
|
89
|
+
`dr whoami --server <key> --account <email>` and **show the operator the org name
|
|
90
|
+
and id before anything else.** Getting this wrong is the single worst outcome here:
|
|
91
|
+
every write lands on someone else's customer. Use `dr login-as <customer-email>`
|
|
92
|
+
for a customer tenant (see `dr-switch-user`).
|
|
93
|
+
3. **Feature flags.** `fx_enabled` and `fx_dashboard_selector` must be on for the org:
|
|
94
|
+
`dr features orgs list <org_id>` — a flag's *presence* means enabled. Without them
|
|
95
|
+
the customer sees nothing after all this work. Enabling them is
|
|
96
|
+
`dr features orgs add`, and it is a decision for the PM/CS owner, not this skill.
|
|
97
|
+
4. **The tenant's backend supports FX.** Run `dr fx default-currency`. Be careful how
|
|
98
|
+
you read it: the CLI treats a missing currency endpoint as "no currencies", so
|
|
99
|
+
*"No reporting currencies available"* means **either** a pre-FX server **or** simply
|
|
100
|
+
that no reporting fields are tagged yet — the command cannot tell you which, and says
|
|
101
|
+
so. On a fresh customer that message is expected, not a blocker. To distinguish the
|
|
102
|
+
two, check whether an FX-era build is deployed (ask R&D or check the release) before
|
|
103
|
+
concluding the tenant cannot support FX.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Step 1 — Map the org
|
|
108
|
+
|
|
109
|
+
Read-only. Build the picture before proposing anything.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
dr templates list # tables, with ids
|
|
113
|
+
dr dashboards list # dashboards
|
|
114
|
+
dr functions list # DR.GET functions, org-wide
|
|
115
|
+
dr fx default-currency # current default + what the selector can offer
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Then, for each table that looks financial, `dr templates get <id> --json` and read the
|
|
119
|
+
fields. You are looking for:
|
|
120
|
+
|
|
121
|
+
- **amount-shaped columns** — `Posting Amount`, `Amount`, `Reporting Amount`,
|
|
122
|
+
`<CODE> Amount`, and anything else numeric that holds money.
|
|
123
|
+
- **currency columns** — `Posting Currency`, `Reporting Currency`. These are the
|
|
124
|
+
evidence for step 2.
|
|
125
|
+
- **rate columns** — `Currency EOP rate`, `Currency AVG rate`, `<CODE> rate`. Their
|
|
126
|
+
presence means the customer is already doing FX by hand.
|
|
127
|
+
|
|
128
|
+
### Stop and look for existing FX machinery
|
|
129
|
+
|
|
130
|
+
**This is the one that can corrupt a customer's numbers.** Before proposing anything,
|
|
131
|
+
check whether the org already converts currency manually:
|
|
132
|
+
|
|
133
|
+
- calculated fields whose formula multiplies an amount by a rate —
|
|
134
|
+
`dr calc-vals list --table <id> --json`, looking for formulas like
|
|
135
|
+
`[Posting Amount] * [Currency EOP rate]`;
|
|
136
|
+
- FX lookup tables — `dr lut list --table <id>` has an `IS_FX` column;
|
|
137
|
+
- amount columns that are already per-currency (`USD Amount`, `EUR Amount`).
|
|
138
|
+
|
|
139
|
+
If any exist, **say so in the plan and do not proceed on those tables without a human
|
|
140
|
+
decision.** Making a field currency-aware when the data is already converted
|
|
141
|
+
double-converts it. Silent wrong numbers are worse than no feature.
|
|
142
|
+
|
|
143
|
+
### Coverage across tables
|
|
144
|
+
|
|
145
|
+
A dashboard's selector offers a currency only where the data exists. For each
|
|
146
|
+
dashboard, note which tables feed its widgets, and compute the **intersection** of
|
|
147
|
+
reporting currencies across those tables. A currency present in one table but missing
|
|
148
|
+
in another shows the end user a "currency is missing" card on half the dashboard. Call
|
|
149
|
+
that out in the plan — it is a data gap, not a config bug.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Step 2 — Decide the currency semantics per table
|
|
154
|
+
|
|
155
|
+
For each FX table, two questions.
|
|
156
|
+
|
|
157
|
+
**Which column is the posting amount?** Usually the one named `Posting Amount` — the
|
|
158
|
+
original, mixed-currency figure. `dr fx set-posting` finds it by name.
|
|
159
|
+
|
|
160
|
+
**What currency is each reporting column in?** `set-reporting` answers this itself
|
|
161
|
+
where the evidence exists — existing config, then a code in the field name, then a
|
|
162
|
+
single-valued "Reporting Currency" column — and its dry run prints **which** of those
|
|
163
|
+
it used per field in the `SOURCE` column (`existing`, `field-name`,
|
|
164
|
+
`reporting-currency-values`). Read that column rather than re-deriving the rules here;
|
|
165
|
+
`dr fx set-reporting --help` is their definition.
|
|
166
|
+
|
|
167
|
+
Your job is the case the command deliberately refuses:
|
|
168
|
+
|
|
169
|
+
**Nothing says so → ask.** A table the sweep lists with `!` is one it would not guess
|
|
170
|
+
on. Do not infer from the rate columns, the customer's country, or the biggest number.
|
|
171
|
+
Put the question to the CSM or the customer: *"Your `Amount` column — which currency
|
|
172
|
+
are those figures in?"* An honest blocker beats a plausible guess, because a wrong
|
|
173
|
+
currency code mislabels real money and nobody downstream can tell. Resolve it with
|
|
174
|
+
`--map "<field>=<CODE>"` once a human answers.
|
|
175
|
+
|
|
176
|
+
Sample rows when it helps a human decide — `dr templates sample <id> --rows 20` —
|
|
177
|
+
but sampling shows magnitudes, never a currency. Magnitude is a hint for a human, not
|
|
178
|
+
a conclusion for you.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Step 3 — Propose the plan, then stop
|
|
183
|
+
|
|
184
|
+
Write the plan out and **wait for approval.** Use the CLI's own dry runs as the
|
|
185
|
+
evidence — they are exact, and they are free:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
dr fx set-posting --all-tables
|
|
189
|
+
dr fx set-reporting --all-tables
|
|
190
|
+
dr fx convert-widgets --all
|
|
191
|
+
dr fx convert-functions
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Every one of those is read-only until confirmed. The field commands print a plan and a
|
|
195
|
+
`--confirm-token`; the converters print a plan and need `--confirm`.
|
|
196
|
+
|
|
197
|
+
The plan you show the implementor must contain:
|
|
198
|
+
|
|
199
|
+
- **the tenant** — org name and id, and the identity that will write;
|
|
200
|
+
- **per table**: which field becomes posting, which become reporting and in which
|
|
201
|
+
currency, **and where that currency came from** (name / data / the customer told us);
|
|
202
|
+
- **tables needing a human decision**, quoted from the dry run's `!` lines, with the
|
|
203
|
+
question to ask;
|
|
204
|
+
- **existing manual FX machinery** found in step 1, and what it means;
|
|
205
|
+
- **currency coverage gaps** across each dashboard's tables;
|
|
206
|
+
- **counts** of widgets and functions that will convert, plus how many are skipped and
|
|
207
|
+
why (`needs-sum`, `needs-fx-role`);
|
|
208
|
+
- **the org default currency** you propose, and that it is what AI agents fall back to;
|
|
209
|
+
- **what is NOT covered by the CLI** and stays a manual UI step (see below);
|
|
210
|
+
- **the rollback** for each phase.
|
|
211
|
+
|
|
212
|
+
Do not fold the "needs a human" items into the write step. They are the plan's most
|
|
213
|
+
important output.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Step 4 — Execute, in order, on approval
|
|
218
|
+
|
|
219
|
+
Per table or org-wide with `--all-tables`. Each field command is its own two-step
|
|
220
|
+
token: run it, read the plan, re-run with the printed `--confirm-token`.
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# 1. posting amounts
|
|
224
|
+
dr fx set-posting --all-tables # plan
|
|
225
|
+
dr fx set-posting --all-tables --confirm-token <t> # apply
|
|
226
|
+
|
|
227
|
+
# 2. reporting amounts (add --map "<field>=<CODE>" for tables the sweep could not judge)
|
|
228
|
+
dr fx set-reporting --all-tables
|
|
229
|
+
dr fx set-reporting --all-tables --confirm-token <t>
|
|
230
|
+
|
|
231
|
+
# a table the sweep skipped, resolved by hand:
|
|
232
|
+
dr fx set-reporting <table_id> --map "Amount=GBP"
|
|
233
|
+
|
|
234
|
+
# a custom currency the ISO list lacks (e.g. budgeted USD):
|
|
235
|
+
dr fx add-currency BUSD --symbol 'B$'
|
|
236
|
+
|
|
237
|
+
# 3. the org default (Super Admin)
|
|
238
|
+
dr fx default-currency USD --confirm
|
|
239
|
+
|
|
240
|
+
# 4. the sweeps
|
|
241
|
+
dr fx convert-widgets --all --confirm
|
|
242
|
+
dr fx convert-functions --confirm
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Rules while executing:
|
|
246
|
+
|
|
247
|
+
- **Never pass `--force-sum` without saying so in the plan.** It changes a widget's
|
|
248
|
+
aggregator to SUM, which changes what the customer sees.
|
|
249
|
+
- **Never use `--allow-custom` or `--allow-unlisted` to get past a validation error.**
|
|
250
|
+
They exist for a code you know is coming, not to silence a refusal.
|
|
251
|
+
- **Re-runs are safe.** Every command is idempotent — an already-configured field
|
|
252
|
+
reports `unchanged`, an already-aware widget reports `already`. If a run half-fails,
|
|
253
|
+
fix the cause and run it again; do not hand-patch.
|
|
254
|
+
- **A failure is not a reason to skip.** `needs-fx-role` means step 1 or 2 is
|
|
255
|
+
incomplete for that field. Go back rather than forcing.
|
|
256
|
+
|
|
257
|
+
### Capture a rollback point first
|
|
258
|
+
|
|
259
|
+
For the widget sweep, save the before-state so there is a restore path beyond the flag:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
dr widgets get <id> -d <dashboard> --json > before-widget-<id>.json
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Step 5 — Verify, and report
|
|
268
|
+
|
|
269
|
+
Do not stop at "the commands succeeded".
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
dr fx set-posting --all-tables # expect: nothing left to set
|
|
273
|
+
dr fx set-reporting --all-tables # expect: nothing left to update
|
|
274
|
+
dr fx convert-widgets --all # expect: nothing left to convert
|
|
275
|
+
dr fx convert-functions # expect: nothing left to convert
|
|
276
|
+
dr fx default-currency # expect: the agreed default, and the currency list
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Read the counts the commands print rather than matching exact wording — these summary
|
|
280
|
+
lines belong to the CLI and change with it. What you are checking is that a re-run
|
|
281
|
+
plans no further changes, and that the tables it still lists with `!` are the ones you
|
|
282
|
+
already reported as needing a customer answer.
|
|
283
|
+
|
|
284
|
+
Then confirm it works for a human, not just in the API: open a converted dashboard,
|
|
285
|
+
switch the selector, and check the numbers change and no widget shows the
|
|
286
|
+
"currency is missing" card. A widget whose table lacks the selected currency is a data
|
|
287
|
+
gap to report, not a config error to fix here.
|
|
288
|
+
|
|
289
|
+
Report: what changed (tables, fields, widgets, functions, default), what was skipped
|
|
290
|
+
and why, the open questions for the customer, the manual UI steps still outstanding,
|
|
291
|
+
and the rollback commands.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## What the CLI cannot do — say this out loud
|
|
296
|
+
|
|
297
|
+
These stay manual, and omitting them from the report leaves the job half-done:
|
|
298
|
+
|
|
299
|
+
- **Excel report currency.** `convert-functions` sets the DR.GET flag, but a saved
|
|
300
|
+
Excel report carries its own `currency_code`, and the file-level selector is UI only.
|
|
301
|
+
- **Clearing an FX role.** `set-posting`/`set-reporting` set roles; removing one is the
|
|
302
|
+
Tables UI.
|
|
303
|
+
- **Per-user currency preference.** Users pick their own currency per dashboard; only
|
|
304
|
+
the org default is scriptable.
|
|
305
|
+
|
|
306
|
+
## Rollback
|
|
307
|
+
|
|
308
|
+
- **Widgets / functions:** `dr fx convert-widgets --all --off --confirm` and
|
|
309
|
+
`dr fx convert-functions --off --confirm`. Acts on the stored flag alone, so it also
|
|
310
|
+
clears flags left on entities that no longer qualify.
|
|
311
|
+
- **A widget body:** `dr widgets update <id> -d <dashboard> --file before-widget-<id>.json`.
|
|
312
|
+
- **Org default:** `dr fx default-currency --none --confirm` (back to posting currency)
|
|
313
|
+
or set the previous code.
|
|
314
|
+
- **Field roles:** no CLI path — the Tables UI. Say so rather than implying a clean undo.
|
|
315
|
+
|
|
316
|
+
## Failure modes worth recognising
|
|
317
|
+
|
|
318
|
+
| Symptom | Cause | Fix |
|
|
319
|
+
|---|---|---|
|
|
320
|
+
| Selector shows `N/A` | Widget has no single Monetary value field | Check the widget's value section; only one value field can be currency-aware |
|
|
321
|
+
| Selector dropdown empty | No `reporting_amount` fields tagged, or flags off | Step 2, then check `fx_enabled` |
|
|
322
|
+
| Widget shows "currency is missing" | That table has no reporting field for the selected currency | Data gap — report it, do not force |
|
|
323
|
+
| Everything reports `needs-fx-role` | Converters ran before the field commands | Do step 1–2 first |
|
|
324
|
+
| `needs-sum` | Aggregator is not SUM; conversion is SUM-only | Decide with the owner, then `--force-sum` deliberately |
|
|
325
|
+
| Numbers doubled after enabling | The org already converted manually | Stop, roll back, resolve the existing FX machinery |
|