@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.
Files changed (3) hide show
  1. package/README.md +10 -1
  2. package/dist/cli.js +112 -3
  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 email templates.
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 = [...await validateEnv(".env"), ...validateEmailTemplates(globSync("email-config/templates/**/*.hbs").toSorted())];
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.1.0";
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.1.0",
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
+ }