@raisenow/tamaro-cli 3.1.0-dev.0 → 3.1.0-dev.1
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 +121 -16
- package/package.json +1 -1
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,25 @@ 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
|
+
const response = await this.#epmsClient.get(`organisations/${organisationUuid}/files`).json();
|
|
327
|
+
return Array.isArray(response) ? response : response.data ?? [];
|
|
328
|
+
} catch (error) {
|
|
329
|
+
throw new Error(`Failed to list organisation files for organisation UUID: ${organisationUuid}`, { cause: error });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
319
332
|
async updateTamaro(configName, tag, json) {
|
|
320
333
|
return this.#epmsClient.put(`products/tamaro/${configName}/tags/${tag}`, { json }).json();
|
|
321
334
|
}
|
|
@@ -443,6 +456,99 @@ const assertArchiveTargetDoesNotExist = (archiveTarget) => {
|
|
|
443
456
|
if (fs.existsSync(archiveTarget)) halt(`Archive target already exists: ${archiveTarget}. Please remove it first.`);
|
|
444
457
|
};
|
|
445
458
|
//#endregion
|
|
459
|
+
//#region src/lib/validators/validateDonationReceiptTemplates.ts
|
|
460
|
+
/** Internal docs validation guide for CS */
|
|
461
|
+
const DONATION_RECEIPT_VALIDATION_GUIDE_URL = "https://raisenow.atlassian.net/wiki/spaces/PD/pages/6231687230/Tamaro+CLI+Commands#Donation-tax-receipts";
|
|
462
|
+
const FLAG_KEYS = ["autogeneratedDonationReceiptEnabled", "autogenerated_donation_receipt_enabled"];
|
|
463
|
+
/**
|
|
464
|
+
* Resolve which config.yml key enabled RaiseNow-generated donation tax
|
|
465
|
+
* receipts. Supports both camelCase and snake_case keys used in customer
|
|
466
|
+
* configs; returns undefined when the option is not strictly `true`.
|
|
467
|
+
*/
|
|
468
|
+
const isAutogeneratedDonationReceiptEnabled = (rawConfig) => {
|
|
469
|
+
if (!rawConfig || typeof rawConfig !== "object") return;
|
|
470
|
+
const config = rawConfig;
|
|
471
|
+
for (const key of FLAG_KEYS) if (config[key] === true) return key;
|
|
472
|
+
};
|
|
473
|
+
/**
|
|
474
|
+
* Check that a donation tax receipt PDF template exists for the account in
|
|
475
|
+
* the organisation files list. Matches Hub storage layout:
|
|
476
|
+
* `{accountUuid}/donation-receipt-templates/{language}.pdf`
|
|
477
|
+
*/
|
|
478
|
+
const donationReceiptTemplateExists = (files, accountUuid) => {
|
|
479
|
+
const prefix = `${accountUuid}/donation-receipt-templates/`;
|
|
480
|
+
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("/"));
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* Locate the line of a top-level `key:` assignment in the config.yml source,
|
|
484
|
+
* so the error can be rendered as a code frame pointing at the value.
|
|
485
|
+
*/
|
|
486
|
+
const findKeyLocation$1 = (source, key) => {
|
|
487
|
+
const lines = source.split("\n");
|
|
488
|
+
const assignment = new RegExp(String.raw`^\s*${key}\s*:`);
|
|
489
|
+
const index = lines.findIndex((line) => assignment.test(line));
|
|
490
|
+
if (index === -1) return;
|
|
491
|
+
const line = index + 1;
|
|
492
|
+
const valueStart = lines[index].indexOf(":") + 1;
|
|
493
|
+
return {
|
|
494
|
+
end: {
|
|
495
|
+
column: lines[index].length,
|
|
496
|
+
line
|
|
497
|
+
},
|
|
498
|
+
start: {
|
|
499
|
+
column: valueStart + 1,
|
|
500
|
+
line
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* Format an error for a `key:` assignment as a code frame pointing at the
|
|
506
|
+
* value, so it looks fancy like this:
|
|
507
|
+
*
|
|
508
|
+
* config.yml:51
|
|
509
|
+
* 50 | show_stored_customer_donation_receipt: true
|
|
510
|
+
* 51 | autogeneratedDonationReceiptEnabled: true
|
|
511
|
+
* | ^^^^ No donation tax receipt PDF template found…
|
|
512
|
+
*/
|
|
513
|
+
const formatError$1 = (configYmlPath, source, key, message) => {
|
|
514
|
+
const location = findKeyLocation$1(source, key);
|
|
515
|
+
if (!location) return `${configYmlPath}: ${key}: ${message}`;
|
|
516
|
+
const frame = codeFrameColumns(source, location, {
|
|
517
|
+
forceColor: true,
|
|
518
|
+
highlightCode: true,
|
|
519
|
+
message
|
|
520
|
+
});
|
|
521
|
+
return `\n${configYmlPath}:${location.start.line}\n${frame}`;
|
|
522
|
+
};
|
|
523
|
+
/**
|
|
524
|
+
* Validate that RaiseNow-generated tax receipts are only enabled when each
|
|
525
|
+
* configured EPMS account has a donation tax receipt PDF template uploaded.
|
|
526
|
+
*/
|
|
527
|
+
const validateDonationReceiptTemplates = async (configYmlPath) => {
|
|
528
|
+
if (!fs.existsSync(configYmlPath)) return [];
|
|
529
|
+
const source = fs.readFileSync(configYmlPath, "utf8");
|
|
530
|
+
const flagKey = isAutogeneratedDonationReceiptEnabled(yaml.load(source));
|
|
531
|
+
if (!flagKey) return [];
|
|
532
|
+
const { accountUuidProd, accountUuidStage } = loadTamaroMetadataFromConfig(configYmlPath);
|
|
533
|
+
const errors = [];
|
|
534
|
+
const environments = [{
|
|
535
|
+
accountUuid: accountUuidStage,
|
|
536
|
+
env: "stage",
|
|
537
|
+
label: "Stage"
|
|
538
|
+
}, {
|
|
539
|
+
accountUuid: accountUuidProd,
|
|
540
|
+
env: "prod",
|
|
541
|
+
label: "Prod"
|
|
542
|
+
}];
|
|
543
|
+
for (const { accountUuid, env, label } of environments) {
|
|
544
|
+
if (!accountUuid) continue;
|
|
545
|
+
const epmsClient = epmsAuthenticate(env);
|
|
546
|
+
const organisationUuid = await epmsClient.getOrganisationIdByAccountUuid(accountUuid);
|
|
547
|
+
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}`));
|
|
548
|
+
}
|
|
549
|
+
return errors;
|
|
550
|
+
};
|
|
551
|
+
//#endregion
|
|
446
552
|
//#region src/lib/validators/validateEmailTemplates.ts
|
|
447
553
|
/**
|
|
448
554
|
* Register custom Handlebars helpers - the same, which are used in the Email Service.
|
|
@@ -564,21 +670,16 @@ const TAMARO_CORE_REGISTRY_URL = "https://registry.npmjs.org/@raisenow/tamaro-co
|
|
|
564
670
|
* Check that the configured version exists for "@raisenow/tamaro-core" on npm:
|
|
565
671
|
* as a dist-tag (e.g. "latest", "lts_2026"), an exact version, or a partial
|
|
566
672
|
* version ("2", "2.15") matching at least one published version.
|
|
567
|
-
*
|
|
568
|
-
* Returns undefined when the registry is unreachable, so offline builds are
|
|
569
|
-
* not blocked by this check.
|
|
570
673
|
*/
|
|
571
674
|
const coreVersionExists = async (version) => {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
675
|
+
const metadata = await ky.get(TAMARO_CORE_REGISTRY_URL, {
|
|
676
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
677
|
+
retry: 0,
|
|
678
|
+
timeout: DEFAULT_HTTP_TIMEOUT
|
|
679
|
+
}).json();
|
|
680
|
+
const distTags = Object.keys(metadata["dist-tags"] ?? {});
|
|
681
|
+
const versions = Object.keys(metadata.versions ?? {});
|
|
682
|
+
return distTags.includes(version) || versions.includes(version) || versions.some((existing) => existing.startsWith(`${version}.`));
|
|
582
683
|
};
|
|
583
684
|
/**
|
|
584
685
|
* Locate the line of a `KEY=` assignment in the .env source, so the error can
|
|
@@ -652,7 +753,11 @@ const validateEnv = async (dotEnvPath) => {
|
|
|
652
753
|
* deployed.
|
|
653
754
|
*/
|
|
654
755
|
const validate = async () => {
|
|
655
|
-
const errors = [
|
|
756
|
+
const errors = [
|
|
757
|
+
...await validateEnv(".env"),
|
|
758
|
+
...validateEmailTemplates(globSync("email-config/templates/**/*.hbs").toSorted()),
|
|
759
|
+
...await validateDonationReceiptTemplates("config.yml")
|
|
760
|
+
];
|
|
656
761
|
if (errors.length > 0) {
|
|
657
762
|
logError("❌ Validation failed with the following errors:");
|
|
658
763
|
console.log(errors.map((el) => el.toString()).join("\n"));
|
|
@@ -1329,7 +1434,7 @@ const promptBucketTamaroEmailConfig = async () => {
|
|
|
1329
1434
|
//#endregion
|
|
1330
1435
|
//#region package.json
|
|
1331
1436
|
var name = "@raisenow/tamaro-cli";
|
|
1332
|
-
var version = "3.1.0-dev.
|
|
1437
|
+
var version = "3.1.0-dev.1";
|
|
1333
1438
|
//#endregion
|
|
1334
1439
|
//#region src/cli.ts
|
|
1335
1440
|
const cli = createCommand().name(name).description("CLI for Tamaro Customer Configurations development").version(version, "-v, --version");
|