Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@
"files": [
"dist"
],
"keywords": [
"costa-rica",
"hacienda",
"electronic-invoicing",
"factura-electronica",
"comprobantes-electronicos",
"cli"
],
"scripts": {
"build": "tsup",
"prepublishOnly": "pnpm run build",
"test": "vitest run",
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/commands/commands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ describe("submit command", () => {
expect(args["dry-run"]).toBeDefined();
expect(args.json).toBeDefined();
});

it("has profile, p12, and pin args", async () => {
const resolved = await resolveCommand(submitCommand);
const args = resolved.args as Record<string, { type: string }>;
expect(args.profile).toBeDefined();
expect(args.p12).toBeDefined();
expect(args.pin).toBeDefined();
});
});

// ---------------------------------------------------------------------------
Expand All @@ -128,6 +136,12 @@ describe("status command", () => {
expect(args.clave.type).toBe("positional");
expect(args.clave.required).toBe(true);
});

it("has profile arg", async () => {
const resolved = await resolveCommand(statusCommand);
const args = resolved.args as Record<string, { type: string }>;
expect(args.profile).toBeDefined();
});
});

// ---------------------------------------------------------------------------
Expand All @@ -147,6 +161,12 @@ describe("list command", () => {
expect(args.limit).toBeDefined();
expect(args.offset).toBeDefined();
});

it("has profile arg", async () => {
const resolved = await resolveCommand(listCommand);
const args = resolved.args as Record<string, { type: string }>;
expect(args.profile).toBeDefined();
});
});

// ---------------------------------------------------------------------------
Expand All @@ -167,6 +187,12 @@ describe("get command", () => {
expect(args.clave.type).toBe("positional");
expect(args.clave.required).toBe(true);
});

it("has profile arg", async () => {
const resolved = await resolveCommand(getCommand);
const args = resolved.args as Record<string, { type: string }>;
expect(args.profile).toBeDefined();
});
});

// ---------------------------------------------------------------------------
Expand Down
44 changes: 30 additions & 14 deletions packages/cli/src/commands/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
* `hacienda get` command.
*
* Gets full details of a document by its clave.
* Currently stubbed — requires authenticated API client.
*
* @module commands/get
*/

import { defineCommand } from "citty";
import { parseClave } from "@hacienda-cr/sdk";
import { warn, error, detail, info, outputJson } from "../utils/format.js";
import { parseClave, getComprobante } from "@hacienda-cr/sdk";
import { error, detail, info, outputJson, colorStatus } from "../utils/format.js";
import { createAuthenticatedClient } from "../utils/api-client.js";

export const getCommand = defineCommand({
meta: {
Expand All @@ -22,6 +22,11 @@ export const getCommand = defineCommand({
description: "50-digit clave numerica",
required: true,
},
profile: {
type: "string",
description: "Config profile name",
default: "default",
},
json: {
type: "boolean",
description: "Output as JSON",
Expand All @@ -39,15 +44,21 @@ export const getCommand = defineCommand({
return;
}

// Parse the clave to show available info
// Parse the clave for supplementary info
const parsed = parseClave(clave);

// API document retrieval requires authentication — show parsed clave
warn("Document retrieval requires authentication. Run `hacienda auth login` first.");
// Authenticate and fetch document
const { httpClient } = await createAuthenticatedClient(args.profile as string);
const doc = await getComprobante(httpClient, clave);

if (args.json) {
outputJson({
clave,
clave: doc.clave,
fechaEmision: doc.fechaEmision,
estado: doc.estado,
emisor: doc.emisor,
receptor: doc.receptor,
fechaRespuesta: doc.fechaRespuesta,
parsed: {
countryCode: parsed.countryCode,
date: parsed.date.toISOString(),
Expand All @@ -57,18 +68,23 @@ export const getCommand = defineCommand({
situation: parsed.situation,
securityCode: parsed.securityCode,
},
message: "API document retrieval not yet implemented",
});
} else {
info(`Document: ${clave}`);
detail("Country Code", parsed.countryCode);
detail("Date", parsed.date.toISOString().slice(0, 10));
detail("Taxpayer ID", parsed.taxpayerId);
info(`Document: ${doc.clave}`);
console.log(` Status: ${colorStatus(doc.estado)}`);
detail("Emission Date", doc.fechaEmision);
detail("Emisor", `${doc.emisor.tipoIdentificacion}: ${doc.emisor.numeroIdentificacion}`);
if (doc.receptor) {
detail(
"Receptor",
`${doc.receptor.tipoIdentificacion}: ${doc.receptor.numeroIdentificacion}`,
);
}
if (doc.fechaRespuesta) detail("Response Date", doc.fechaRespuesta);
console.log("");
detail("Document Type", parsed.documentType);
detail("Sequence", String(parsed.sequence));
detail("Situation", parsed.situation);
detail("Security Code", parsed.securityCode);
console.log("\nFull document details will be available after authentication.");
}
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error occurred";
Expand Down
93 changes: 57 additions & 36 deletions packages/cli/src/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
* `hacienda list` command.
*
* Lists recent comprobantes from the Hacienda API.
* Currently stubbed — requires authenticated API client.
*
* @module commands/list
*/

import { defineCommand } from "citty";
import { warn, outputJson, formatTable, colorStatus } from "../utils/format.js";
import { listComprobantes } from "@hacienda-cr/sdk";
import { error, outputJson, formatTable, colorStatus } from "../utils/format.js";
import type { TableColumn } from "../utils/format.js";
import { createAuthenticatedClient } from "../utils/api-client.js";

/** Column definitions for the comprobantes table. */
const COMPROBANTE_COLUMNS: TableColumn[] = [
Expand All @@ -21,7 +22,6 @@ const COMPROBANTE_COLUMNS: TableColumn[] = [
minWidth: 12,
format: (v) => colorStatus(String(v)),
},
{ header: "TYPE", key: "type", minWidth: 10 },
];

export const listCommand = defineCommand({
Expand All @@ -40,49 +40,70 @@ export const listCommand = defineCommand({
description: "Pagination offset",
default: "0",
},
profile: {
type: "string",
description: "Config profile name",
default: "default",
},
json: {
type: "boolean",
description: "Output as JSON",
default: false,
},
},
async run({ args }) {
// API listing requires authentication — show guidance
warn(
"Authenticated API listing requires a configured profile. Run `hacienda auth login` first.",
);
try {
const limit = Number(args.limit);
const offset = Number(args.offset);

// Show example of what the output will look like
const exampleData = [
{
clave: "50601012400310123456700100001010000000001199999999",
fechaEmision: "2024-01-12",
estado: "aceptado",
type: "FE",
},
{
clave: "50601012400310123456700100001010000000002199999998",
fechaEmision: "2024-01-12",
estado: "procesando",
type: "FE",
},
];
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
error("Invalid --limit. Must be an integer between 1 and 100.");
process.exitCode = 1;
return;
}
if (!Number.isInteger(offset) || offset < 0) {
error("Invalid --offset. Must be a non-negative integer.");
process.exitCode = 1;
return;
}

if (args.json) {
outputJson({
success: false,
error: "API listing not yet implemented",
exampleFormat: {
totalRegistros: 0,
offset: Number(args.offset),
limit: Number(args.limit),
comprobantes: [],
},
const { httpClient } = await createAuthenticatedClient(args.profile as string);

const result = await listComprobantes(httpClient, {
offset,
limit,
});
} else {
console.log("\nExample output format:\n");
console.log(formatTable(COMPROBANTE_COLUMNS, exampleData));
console.log("\n(This is placeholder data. Run `hacienda auth login` to connect.)");

if (args.json) {
outputJson({
success: true,
totalRegistros: result.totalRegistros,
offset: result.offset,
comprobantes: result.comprobantes,
});
} else {
if (result.comprobantes.length === 0) {
console.log("No comprobantes found.");
} else {
console.log(
`\nShowing ${result.comprobantes.length} of ${result.totalRegistros} comprobantes:\n`,
);
console.log(
formatTable(
COMPROBANTE_COLUMNS,
result.comprobantes as unknown as Record<string, unknown>[],
),
);
}
}
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error occurred";
if (args.json) {
outputJson({ success: false, error: message });
} else {
error(`List failed: ${message}`);
}
process.exitCode = 1;
}
},
});
33 changes: 24 additions & 9 deletions packages/cli/src/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
*/

import { defineCommand } from "citty";
import { parseClave } from "@hacienda-cr/sdk";
import { warn, error, detail, info, outputJson, colorStatus } from "../utils/format.js";
import { parseClave, getStatus, extractRejectionReason } from "@hacienda-cr/sdk";
import { error, detail, info, outputJson, colorStatus } from "../utils/format.js";
import { createAuthenticatedClient } from "../utils/api-client.js";

export const statusCommand = defineCommand({
meta: {
Expand All @@ -21,6 +22,11 @@ export const statusCommand = defineCommand({
description: "50-digit clave numerica",
required: true,
},
profile: {
type: "string",
description: "Config profile name",
default: "default",
},
json: {
type: "boolean",
description: "Output as JSON",
Expand All @@ -41,12 +47,22 @@ export const statusCommand = defineCommand({
// Parse the clave to show its components
const parsed = parseClave(clave);

// API polling requires authentication — show parsed clave
warn("Status polling requires authentication. Run `hacienda auth login` first.");
// Authenticate and query status
const { httpClient } = await createAuthenticatedClient(args.profile as string);
const status = await getStatus(httpClient, clave);

// Extract rejection reason if available
let rejectionReason: string | undefined;
if (status.responseXml) {
rejectionReason = extractRejectionReason(status.responseXml);
}

if (args.json) {
outputJson({
clave,
status: status.status,
date: status.date,
rejectionReason,
parsed: {
countryCode: parsed.countryCode,
date: parsed.date.toISOString(),
Expand All @@ -56,19 +72,18 @@ export const statusCommand = defineCommand({
situation: parsed.situation,
securityCode: parsed.securityCode,
},
status: "unknown",
message: "API status polling not yet implemented",
});
} else {
info(`Clave: ${clave}`);
console.log(` Status: ${colorStatus(status.status)}`);
if (status.date) detail("Date", status.date);
if (rejectionReason) detail("Reason", rejectionReason);
console.log("");
detail("Country Code", parsed.countryCode);
detail("Date", parsed.date.toISOString().slice(0, 10));
detail("Taxpayer ID", parsed.taxpayerId);
detail("Document Type", parsed.documentType);
detail("Sequence", String(parsed.sequence));
detail("Situation", parsed.situation);
detail("Security Code", parsed.securityCode);
console.log(`\n Status: ${colorStatus("unknown")} (requires authentication)`);
}
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error occurred";
Expand Down
Loading