@raisenow/tamaro-cli 3.1.0 → 3.2.0
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 +10 -1
- package/dist/cli.js +112 -3
- package/package.json +15 -14
package/README.md
CHANGED
|
@@ -282,7 +282,16 @@ pnpx @raisenow/tamaro-cli update-epms --tag WEB-123
|
|
|
282
282
|
|
|
283
283
|
## `validate`
|
|
284
284
|
|
|
285
|
-
Validate the current configuration and fail on known issues such as invalid
|
|
285
|
+
Validate the current configuration and fail on known issues such as invalid
|
|
286
|
+
email templates, invalid `.env` values, or RaiseNow-generated tax receipts
|
|
287
|
+
enabled without an uploaded organisation template.
|
|
288
|
+
|
|
289
|
+
When `autogeneratedDonationReceiptEnabled` (or
|
|
290
|
+
`autogenerated_donation_receipt_enabled`) is `true`, this command authenticates
|
|
291
|
+
with EPMS (browser login or client credentials) and checks that a donation
|
|
292
|
+
tax-receipt PDF template exists for each configured account. See the
|
|
293
|
+
[Confluence guide](https://raisenow.atlassian.net/wiki/spaces/PD/pages/6231687230/Tamaro+CLI+Commands#Tax-receipt-validation--how-CS-resolves-it)
|
|
294
|
+
for how to resolve a failed check.
|
|
286
295
|
|
|
287
296
|
### Example
|
|
288
297
|
|
package/dist/cli.js
CHANGED
|
@@ -12,9 +12,9 @@ import ky from "ky";
|
|
|
12
12
|
import { createServer } from "node:http";
|
|
13
13
|
import open from "open";
|
|
14
14
|
import { globSync } from "glob";
|
|
15
|
+
import { codeFrameColumns } from "@babel/code-frame";
|
|
15
16
|
import Handlebars from "handlebars";
|
|
16
17
|
import helpers from "handlebars-helpers";
|
|
17
|
-
import { codeFrameColumns } from "@babel/code-frame";
|
|
18
18
|
import { config, parse } from "dotenv";
|
|
19
19
|
import { getPortPromise } from "portfinder";
|
|
20
20
|
import prompts from "prompts";
|
|
@@ -280,6 +280,8 @@ var EpmsClient = class EpmsClient {
|
|
|
280
280
|
#cacheKey;
|
|
281
281
|
#epmsClient;
|
|
282
282
|
#authType;
|
|
283
|
+
/** In-memory account UUID → organisation UUID cache for this client instance. */
|
|
284
|
+
#organisationIdByAccountUuid = /* @__PURE__ */ new Map();
|
|
283
285
|
/**
|
|
284
286
|
* Creates an instance of EpmsClient.
|
|
285
287
|
*
|
|
@@ -308,14 +310,24 @@ var EpmsClient = class EpmsClient {
|
|
|
308
310
|
return this.#epmsClient.get("users/myself").json().then((response) => myselfSchema.parse(response));
|
|
309
311
|
}
|
|
310
312
|
async getOrganisationIdByAccountUuid(accountUuid) {
|
|
313
|
+
const cached = this.#organisationIdByAccountUuid.get(accountUuid);
|
|
314
|
+
if (cached) return cached;
|
|
311
315
|
try {
|
|
312
316
|
const organisationUuid = (await this.#epmsClient.get(`accounts/${accountUuid}`).json()).organisation.uuid;
|
|
313
317
|
if (!organisationUuid) throw new Error("Organisation UUID not found in response");
|
|
318
|
+
this.#organisationIdByAccountUuid.set(accountUuid, organisationUuid);
|
|
314
319
|
return organisationUuid;
|
|
315
320
|
} catch (error) {
|
|
316
321
|
throw new Error(`Organisation UUID not found for account UUID: ${accountUuid}`, { cause: error });
|
|
317
322
|
}
|
|
318
323
|
}
|
|
324
|
+
async listOrganisationFiles(organisationUuid) {
|
|
325
|
+
try {
|
|
326
|
+
return await this.#epmsClient.get(`organisations/${organisationUuid}/files`).json();
|
|
327
|
+
} catch (error) {
|
|
328
|
+
throw new Error(`Failed to list organisation files for organisation UUID: ${organisationUuid}`, { cause: error });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
319
331
|
async updateTamaro(configName, tag, json) {
|
|
320
332
|
return this.#epmsClient.put(`products/tamaro/${configName}/tags/${tag}`, { json }).json();
|
|
321
333
|
}
|
|
@@ -443,6 +455,99 @@ const assertArchiveTargetDoesNotExist = (archiveTarget) => {
|
|
|
443
455
|
if (fs.existsSync(archiveTarget)) halt(`Archive target already exists: ${archiveTarget}. Please remove it first.`);
|
|
444
456
|
};
|
|
445
457
|
//#endregion
|
|
458
|
+
//#region src/lib/validators/validateDonationReceiptTemplates.ts
|
|
459
|
+
/** Internal docs validation guide for CS */
|
|
460
|
+
const DONATION_RECEIPT_VALIDATION_GUIDE_URL = "https://raisenow.atlassian.net/wiki/spaces/PD/pages/6231687230/Tamaro+CLI+Commands#Donation-tax-receipts";
|
|
461
|
+
const FLAG_KEYS = ["autogeneratedDonationReceiptEnabled", "autogenerated_donation_receipt_enabled"];
|
|
462
|
+
/**
|
|
463
|
+
* Resolve which config.yml key enabled RaiseNow-generated donation tax
|
|
464
|
+
* receipts. Supports both camelCase and snake_case keys used in customer
|
|
465
|
+
* configs; returns undefined when the option is not strictly `true`.
|
|
466
|
+
*/
|
|
467
|
+
const isAutogeneratedDonationReceiptEnabled = (rawConfig) => {
|
|
468
|
+
if (!rawConfig || typeof rawConfig !== "object") return;
|
|
469
|
+
const config = rawConfig;
|
|
470
|
+
for (const key of FLAG_KEYS) if (config[key] === true) return key;
|
|
471
|
+
};
|
|
472
|
+
/**
|
|
473
|
+
* Check that a donation tax receipt PDF template exists for the account in
|
|
474
|
+
* the organisation files list. Matches Hub storage layout:
|
|
475
|
+
* `{accountUuid}/donation-receipt-templates/{language}.pdf`
|
|
476
|
+
*/
|
|
477
|
+
const donationReceiptTemplateExists = (files, accountUuid) => {
|
|
478
|
+
const prefix = `${accountUuid}/donation-receipt-templates/`;
|
|
479
|
+
return files.some((file) => file.name.startsWith(prefix) && file.name.endsWith(".pdf") && file.name.slice(prefix.length).length > 0 && !file.name.slice(prefix.length).includes("/"));
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* Locate the line of a top-level `key:` assignment in the config.yml source,
|
|
483
|
+
* so the error can be rendered as a code frame pointing at the value.
|
|
484
|
+
*/
|
|
485
|
+
const findKeyLocation$1 = (source, key) => {
|
|
486
|
+
const lines = source.split("\n");
|
|
487
|
+
const assignment = new RegExp(String.raw`^\s*${key}\s*:`);
|
|
488
|
+
const index = lines.findIndex((line) => assignment.test(line));
|
|
489
|
+
if (index === -1) return;
|
|
490
|
+
const line = index + 1;
|
|
491
|
+
const valueStart = lines[index].indexOf(":") + 1;
|
|
492
|
+
return {
|
|
493
|
+
end: {
|
|
494
|
+
column: lines[index].length,
|
|
495
|
+
line
|
|
496
|
+
},
|
|
497
|
+
start: {
|
|
498
|
+
column: valueStart + 1,
|
|
499
|
+
line
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
};
|
|
503
|
+
/**
|
|
504
|
+
* Format an error for a `key:` assignment as a code frame pointing at the
|
|
505
|
+
* value, so it looks fancy like this:
|
|
506
|
+
*
|
|
507
|
+
* config.yml:51
|
|
508
|
+
* 50 | show_stored_customer_donation_receipt: true
|
|
509
|
+
* 51 | autogeneratedDonationReceiptEnabled: true
|
|
510
|
+
* | ^^^^ No donation tax receipt PDF template found…
|
|
511
|
+
*/
|
|
512
|
+
const formatError$1 = (configYmlPath, source, key, message) => {
|
|
513
|
+
const location = findKeyLocation$1(source, key);
|
|
514
|
+
if (!location) return `${configYmlPath}: ${key}: ${message}`;
|
|
515
|
+
const frame = codeFrameColumns(source, location, {
|
|
516
|
+
forceColor: true,
|
|
517
|
+
highlightCode: true,
|
|
518
|
+
message
|
|
519
|
+
});
|
|
520
|
+
return `\n${configYmlPath}:${location.start.line}\n${frame}`;
|
|
521
|
+
};
|
|
522
|
+
/**
|
|
523
|
+
* Validate that RaiseNow-generated tax receipts are only enabled when each
|
|
524
|
+
* configured EPMS account has a donation tax receipt PDF template uploaded.
|
|
525
|
+
*/
|
|
526
|
+
const validateDonationReceiptTemplates = async (configYmlPath) => {
|
|
527
|
+
if (!fs.existsSync(configYmlPath)) return [];
|
|
528
|
+
const source = fs.readFileSync(configYmlPath, "utf8");
|
|
529
|
+
const flagKey = isAutogeneratedDonationReceiptEnabled(yaml.load(source));
|
|
530
|
+
if (!flagKey) return [];
|
|
531
|
+
const { accountUuidProd, accountUuidStage } = loadTamaroMetadataFromConfig(configYmlPath);
|
|
532
|
+
const errors = [];
|
|
533
|
+
const environments = [{
|
|
534
|
+
accountUuid: accountUuidStage,
|
|
535
|
+
env: "stage",
|
|
536
|
+
label: "Stage"
|
|
537
|
+
}, {
|
|
538
|
+
accountUuid: accountUuidProd,
|
|
539
|
+
env: "prod",
|
|
540
|
+
label: "Prod"
|
|
541
|
+
}];
|
|
542
|
+
for (const { accountUuid, env, label } of environments) {
|
|
543
|
+
if (!accountUuid) continue;
|
|
544
|
+
const epmsClient = epmsAuthenticate(env);
|
|
545
|
+
const organisationUuid = await epmsClient.getOrganisationIdByAccountUuid(accountUuid);
|
|
546
|
+
if (!donationReceiptTemplateExists(await epmsClient.listOrganisationFiles(organisationUuid), accountUuid)) errors.push(formatError$1(configYmlPath, source, flagKey, `No donation tax receipt PDF template found for the ${label} account (${accountUuid}). See: ${DONATION_RECEIPT_VALIDATION_GUIDE_URL}`));
|
|
547
|
+
}
|
|
548
|
+
return errors;
|
|
549
|
+
};
|
|
550
|
+
//#endregion
|
|
446
551
|
//#region src/lib/validators/validateEmailTemplates.ts
|
|
447
552
|
/**
|
|
448
553
|
* Register custom Handlebars helpers - the same, which are used in the Email Service.
|
|
@@ -647,7 +752,11 @@ const validateEnv = async (dotEnvPath) => {
|
|
|
647
752
|
* deployed.
|
|
648
753
|
*/
|
|
649
754
|
const validate = async () => {
|
|
650
|
-
const errors = [
|
|
755
|
+
const errors = [
|
|
756
|
+
...await validateEnv(".env"),
|
|
757
|
+
...validateEmailTemplates(globSync("email-config/templates/**/*.hbs").toSorted()),
|
|
758
|
+
...await validateDonationReceiptTemplates("config.yml")
|
|
759
|
+
];
|
|
651
760
|
if (errors.length > 0) {
|
|
652
761
|
logError("❌ Validation failed with the following errors:");
|
|
653
762
|
console.log(errors.map((el) => el.toString()).join("\n"));
|
|
@@ -1324,7 +1433,7 @@ const promptBucketTamaroEmailConfig = async () => {
|
|
|
1324
1433
|
//#endregion
|
|
1325
1434
|
//#region package.json
|
|
1326
1435
|
var name = "@raisenow/tamaro-cli";
|
|
1327
|
-
var version = "3.
|
|
1436
|
+
var version = "3.2.0";
|
|
1328
1437
|
//#endregion
|
|
1329
1438
|
//#region src/cli.ts
|
|
1330
1439
|
const cli = createCommand().name(name).description("CLI for Tamaro Customer Configurations development").version(version, "-v, --version");
|
package/package.json
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raisenow/tamaro-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "RaiseNow",
|
|
6
6
|
"email": "development@raisenow.com"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
|
+
"bin": "dist/cli.js",
|
|
10
|
+
"packageManager": "pnpm@11.10.0",
|
|
9
11
|
"engines": {
|
|
10
12
|
"node": ">=24.18.0",
|
|
11
13
|
"pnpm": ">=11.10.0",
|
|
12
14
|
"npm": "please-use-pnpm",
|
|
13
15
|
"yarn": "please-use-pnpm"
|
|
14
16
|
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"typecheck": "tsc --build --emitDeclarationOnly",
|
|
19
|
+
"build": "tsdown",
|
|
20
|
+
"dev": "pnpm build --watch",
|
|
21
|
+
"readme": "pnpx http-server . --cors -o /readme.html",
|
|
22
|
+
"lint": "DEBUG=eslint:eslint-helpers eslint .",
|
|
23
|
+
"format": "prettier . --write --ignore-path ./.prettierignore",
|
|
24
|
+
"test": "vitest",
|
|
25
|
+
"prepare": "husky",
|
|
26
|
+
"prepublishOnly": "pnpm build"
|
|
27
|
+
},
|
|
15
28
|
"dependencies": {
|
|
16
29
|
"@aws-sdk/client-s3": "^3.1081.0",
|
|
17
30
|
"@babel/code-frame": "^8.0.0",
|
|
@@ -54,17 +67,5 @@
|
|
|
54
67
|
"eslint --fix",
|
|
55
68
|
"prettier --write"
|
|
56
69
|
]
|
|
57
|
-
},
|
|
58
|
-
"scripts": {
|
|
59
|
-
"typecheck": "tsc --build --emitDeclarationOnly",
|
|
60
|
-
"build": "tsdown",
|
|
61
|
-
"dev": "pnpm build --watch",
|
|
62
|
-
"readme": "pnpx http-server . --cors -o /readme.html",
|
|
63
|
-
"lint": "DEBUG=eslint:eslint-helpers eslint .",
|
|
64
|
-
"format": "prettier . --write --ignore-path ./.prettierignore",
|
|
65
|
-
"test": "vitest"
|
|
66
|
-
},
|
|
67
|
-
"bin": {
|
|
68
|
-
"tamaro-cli": "dist/cli.js"
|
|
69
70
|
}
|
|
70
|
-
}
|
|
71
|
+
}
|