@dojocoding/hacienda-mcp 0.4.0 → 0.4.2
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/CHANGELOG.md +19 -0
- package/README.md +66 -8
- package/dist/{chunk-Q2UJO3PQ.js → chunk-A5SYNMZP.js} +74 -18
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @dojocoding/hacienda-mcp
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b6b25b4: Correct shared fiscal document codes, preserve current official schema provenance, enforce document identity and tax consistency, and use Costa Rica calendar dates in claves. Scope MCP invoice numbering by issuer/environment and invalidate authenticated clients after profile or credential changes. Preserve existing HTTP cancellation and redirect protections, make token deadlines configurable, disable mutation replays by default, and redact diagnostic secrets. Add regression coverage and document actual sandbox reception failures separately from offline test success. Live acceptance of all document families remains a release gate.
|
|
8
|
+
- Updated dependencies [b6b25b4]
|
|
9
|
+
- @dojocoding/hacienda-sdk@0.4.2
|
|
10
|
+
- @dojocoding/hacienda-shared@0.4.2
|
|
11
|
+
|
|
12
|
+
## 0.4.1
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- Updated dependencies [7c800ab]
|
|
18
|
+
- Updated dependencies [682bce8]
|
|
19
|
+
- @dojocoding/hacienda-shared@0.4.1
|
|
20
|
+
- @dojocoding/hacienda-sdk@0.4.1
|
|
21
|
+
|
|
3
22
|
## 0.4.0
|
|
4
23
|
|
|
5
24
|
- Declare Node.js 22+ and public package metadata; verify installed TypeScript consumers and ship corrected upgrade documentation.
|
package/README.md
CHANGED
|
@@ -65,23 +65,65 @@ const transport = new StdioServerTransport();
|
|
|
65
65
|
await server.connect(transport);
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
## Authentication and document lifecycle
|
|
69
|
+
|
|
70
|
+
`lookup_taxpayer`, `draft_invoice`, and `create_invoice` do not need IDP
|
|
71
|
+
authentication. `draft_invoice` produces a template with provider and issuer
|
|
72
|
+
location placeholders. Complete these and review the activity, CABYS, taxes,
|
|
73
|
+
and customer before passing it to `create_invoice`.
|
|
74
|
+
|
|
75
|
+
`create_invoice` returns **unsigned XML**, its clave, and calculated totals. It
|
|
76
|
+
does not sign or submit to Hacienda. It increments the default local sequence
|
|
77
|
+
store for document type `01`, branch `001`, POS `00001`; the counter does not
|
|
78
|
+
separate issuers or environments. Save the returned XML and clave before
|
|
79
|
+
handing them to your backend for validation, signing, submission, and status
|
|
80
|
+
tracking. Repeating `create_invoice` allocates a new number and clave.
|
|
81
|
+
|
|
82
|
+
`check_status`, `list_documents`, and `get_document` need a saved profile and
|
|
83
|
+
`HACIENDA_PASSWORD` in the **MCP server process**. First create the profile with
|
|
84
|
+
the CLI (using the corresponding password in its environment):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
hacienda auth login --cedula-type 02 --cedula 3101234567 \
|
|
88
|
+
--environment sandbox --profile sandbox
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Replace the identification with your issuer's. Select `profile: "sandbox"`
|
|
92
|
+
when calling these tools; all three default to `"default"`. A desktop client's
|
|
93
|
+
server may not inherit your terminal's environment. Use your client's secret
|
|
94
|
+
or environment configuration to supply `HACIENDA_PASSWORD` to the server. The
|
|
95
|
+
password is not stored by `auth login`; saving a profile alone is insufficient.
|
|
96
|
+
Restart the server after updating profiles or credentials because authenticated
|
|
97
|
+
clients are cached by profile name.
|
|
98
|
+
|
|
99
|
+
For a complete SDK handoff, see the [sandbox walkthrough](https://github.com/DojoCodingLabs/hacienda-cr/blob/main/docs/sandbox-guide.md#usar-la-cli-o-mcp-en-el-mismo-flujo).
|
|
100
|
+
For multiple companies or application instances, see the
|
|
101
|
+
[production integration guide](https://github.com/DojoCodingLabs/hacienda-cr/blob/main/docs/production-integration.md).
|
|
102
|
+
|
|
68
103
|
## Tools
|
|
69
104
|
|
|
70
105
|
### `create_invoice`
|
|
71
106
|
|
|
72
|
-
Create a Factura Electronica (electronic invoice).
|
|
107
|
+
Create a Factura Electronica (electronic invoice). Automatically computes taxes,
|
|
108
|
+
totals, generates the clave numerica, validates input and XSD, and returns
|
|
109
|
+
unsigned XML. Local validation does not imply Hacienda acceptance.
|
|
73
110
|
|
|
74
111
|
**Parameters:**
|
|
75
112
|
|
|
76
|
-
- `
|
|
113
|
+
- `proveedorSistemas` -- Required identification of the invoicing system provider (9–12 digits)
|
|
114
|
+
- `emisor` -- Issuer information: name, ID, email, and required `ubicacion` with `provincia`, `canton`, `distrito`, `otrasSenas`; optional `nombreComercial`
|
|
77
115
|
- `receptor` -- Receiver information (name, optional ID, optional email)
|
|
78
|
-
- `codigoActividadEmisor` -- Issuer economic activity code (6 digits)
|
|
79
|
-
- `proveedorSistemas` -- Actual invoicing-system provider ID (required; no issuer fallback)
|
|
116
|
+
- `codigoActividadEmisor` -- Issuer economic activity code (6 digits), distinct from each item's 13-digit CABYS code
|
|
80
117
|
- `condicionVenta` -- Sale condition code (default: `"01"` = cash)
|
|
81
|
-
- `medioPago` --
|
|
82
|
-
- `lineItems` -- Array of
|
|
118
|
+
- `medioPago` -- Single payment-method string (default: `"01"` = cash); emitted with its amount inside `ResumenFactura`
|
|
119
|
+
- `lineItems` -- Array of 1–1000 items with `codigoCabys`, `cantidad`, `unidadMedida`, `detalle`, `precioUnitario`, optional `impuesto` (up to 10), `descuento` (up to 5), and `esServicio` (defaults to `false`; set `true` for services)
|
|
83
120
|
- `plazoCredito` -- Credit term in days (optional)
|
|
84
121
|
|
|
122
|
+
`plazoCredito` is a string and is required for credit sales (`condicionVenta: "02"`).
|
|
123
|
+
Tax inputs contain `codigo`, `tarifa`, and optional `codigoTarifaIVA`; discount
|
|
124
|
+
inputs contain `montoDescuento`, `naturalezaDescuento`, and `codigoDescuento` (defaults to `"01"`). The tool calculates
|
|
125
|
+
the resulting amounts.
|
|
126
|
+
|
|
85
127
|
### `check_status`
|
|
86
128
|
|
|
87
129
|
Check the processing status of a document by its 50-digit clave numerica.
|
|
@@ -89,6 +131,7 @@ Check the processing status of a document by its 50-digit clave numerica.
|
|
|
89
131
|
**Parameters:**
|
|
90
132
|
|
|
91
133
|
- `clave` -- The 50-digit clave numerica
|
|
134
|
+
- `profile` -- Saved profile name (default: `"default"`); requires server password environment
|
|
92
135
|
|
|
93
136
|
### `list_documents`
|
|
94
137
|
|
|
@@ -102,6 +145,7 @@ List recent electronic documents with optional filters.
|
|
|
102
145
|
- `receptorIdentificacion` -- Filter by receiver ID (optional)
|
|
103
146
|
- `fechaDesde` -- Start date filter, ISO 8601 (optional)
|
|
104
147
|
- `fechaHasta` -- End date filter, ISO 8601 (optional)
|
|
148
|
+
- `profile` -- Saved profile name (default: `"default"`); requires server password environment
|
|
105
149
|
|
|
106
150
|
### `get_document`
|
|
107
151
|
|
|
@@ -110,6 +154,7 @@ Get full details of an electronic document by its 50-digit clave numerica.
|
|
|
110
154
|
**Parameters:**
|
|
111
155
|
|
|
112
156
|
- `clave` -- The 50-digit clave numerica
|
|
157
|
+
- `profile` -- Saved profile name (default: `"default"`); requires server password environment
|
|
113
158
|
|
|
114
159
|
### `lookup_taxpayer`
|
|
115
160
|
|
|
@@ -121,7 +166,10 @@ Look up a Costa Rica taxpayer by identification number (cedula). Returns name, I
|
|
|
121
166
|
|
|
122
167
|
### `draft_invoice`
|
|
123
168
|
|
|
124
|
-
Generate a draft invoice template
|
|
169
|
+
Generate a draft invoice template. Fill in `proveedorSistemas` and issuer
|
|
170
|
+
location placeholders, review the default CABYS/activity and tax inputs, then
|
|
171
|
+
pass the completed JSON to `create_invoice`. It is not the full invoice JSON
|
|
172
|
+
accepted by CLI `submit`.
|
|
125
173
|
|
|
126
174
|
**Parameters:**
|
|
127
175
|
|
|
@@ -149,8 +197,18 @@ Generate a draft invoice template with sensible defaults. Returns JSON that can
|
|
|
149
197
|
|
|
150
198
|
## Full Documentation
|
|
151
199
|
|
|
152
|
-
See the [
|
|
200
|
+
See the [MCP reference](https://github.com/DojoCodingLabs/hacienda-cr/blob/main/docs/reference.md#mcp-server--integración-con-ia),
|
|
201
|
+
[sandbox walkthrough](https://github.com/DojoCodingLabs/hacienda-cr/blob/main/docs/sandbox-guide.md), and
|
|
202
|
+
[production integration guide](https://github.com/DojoCodingLabs/hacienda-cr/blob/main/docs/production-integration.md).
|
|
153
203
|
|
|
154
204
|
## Migrating to 0.4.0
|
|
155
205
|
|
|
156
206
|
Read the [migration guide](https://github.com/DojoCodingLabs/hacienda-cr/blob/main/packages/sdk/MIGRATION-v4.4.md) before upgrading.
|
|
207
|
+
|
|
208
|
+
### Issuer numbering and credential rotation
|
|
209
|
+
|
|
210
|
+
`create_invoice` accepts `environment` (defaults to `sandbox`) and stores local counters under `issuer-sequences/<environment>/<sha256 identity>/sequences.json` in the configuration directory. The digest covers environment, identification type and identification number. Branch, terminal and document type remain separate counters within that file. This is local-process/file coordination, not a distributed transactional counter.
|
|
211
|
+
|
|
212
|
+
If an old global `sequences.json` contains positive counters, creating a new unseeded issuer series fails closed. Stop writers, back up the old file, establish the issuer/environment that owns each series, and seed each corresponding scoped file with the last reserved number before resuming. Do not copy a mixed-company global file blindly, delete counters, or restart a production series at one. There is no automatic migration because the old file does not identify its owner.
|
|
213
|
+
|
|
214
|
+
Authenticated clients are invalidated when the resolved profile, environment, issuer configuration, certificate path, password or PIN changes. Replacing certificate bytes at the same path still requires restarting the process or clearing the cache. Configure one profile per identity/environment.
|
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
|
|
5
|
+
// src/tools/issuer-sequence.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
import { readFile, access } from "fs/promises";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { getConfigDir, getNextSequence } from "@dojocoding/hacienda-sdk";
|
|
10
|
+
function issuerSequenceDirectory(environment, idType, idNumber, configDir) {
|
|
11
|
+
const identity = createHash("sha256").update(JSON.stringify([environment, idType, idNumber])).digest("hex");
|
|
12
|
+
return join(getConfigDir(configDir), "issuer-sequences", environment, identity);
|
|
13
|
+
}
|
|
14
|
+
async function nextIssuerSequence(environment, idType, idNumber, documentType, branch, pos, configDir) {
|
|
15
|
+
const destination = issuerSequenceDirectory(environment, idType, idNumber, configDir);
|
|
16
|
+
try {
|
|
17
|
+
const legacy = JSON.parse(
|
|
18
|
+
await readFile(join(getConfigDir(configDir), "sequences.json"), "utf8")
|
|
19
|
+
);
|
|
20
|
+
if (legacy && typeof legacy === "object" && Object.values(legacy).some((v) => typeof v === "number" && v > 0)) {
|
|
21
|
+
try {
|
|
22
|
+
await access(join(destination, "sequences.json"));
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"Legacy numbering exists. Migrate and seed the issuer/environment series before creating a document."
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
31
|
+
}
|
|
32
|
+
return getNextSequence(documentType, branch, pos, { configDir: destination });
|
|
33
|
+
}
|
|
34
|
+
|
|
5
35
|
// src/tools/create-invoice.ts
|
|
6
36
|
import { z } from "zod";
|
|
7
37
|
import {
|
|
@@ -13,7 +43,7 @@ import {
|
|
|
13
43
|
Situation,
|
|
14
44
|
calculateLineItemTotals,
|
|
15
45
|
calculateInvoiceSummary,
|
|
16
|
-
|
|
46
|
+
sanitizeLogString,
|
|
17
47
|
DEFAULT_BRANCH,
|
|
18
48
|
DEFAULT_POS
|
|
19
49
|
} from "@dojocoding/hacienda-sdk";
|
|
@@ -62,6 +92,7 @@ var ReceptorInputSchema = z.object({
|
|
|
62
92
|
correoElectronico: z.string().optional().describe("Email address")
|
|
63
93
|
});
|
|
64
94
|
var CreateInvoiceInputSchema = z.object({
|
|
95
|
+
environment: z.enum(["sandbox", "production"]).default("sandbox"),
|
|
65
96
|
emisor: EmisorInputSchema.describe("Invoice issuer (emisor)"),
|
|
66
97
|
receptor: ReceptorInputSchema.describe("Invoice receiver (receptor)"),
|
|
67
98
|
proveedorSistemas: z.string().regex(/^\d{9,12}$/).describe("Actual invoicing-system provider identification number (ProveedorSistemas)"),
|
|
@@ -78,6 +109,7 @@ function registerCreateInvoiceTool(server) {
|
|
|
78
109
|
"create_invoice",
|
|
79
110
|
"Create a Factura Electronica (electronic invoice) for Costa Rica. Accepts emisor, receptor, and line items. Automatically computes taxes, totals, generates the clave numerica, and builds the XML. Returns the unsigned XML string.",
|
|
80
111
|
{
|
|
112
|
+
environment: CreateInvoiceInputSchema.shape.environment,
|
|
81
113
|
emisor: EmisorInputSchema,
|
|
82
114
|
receptor: ReceptorInputSchema,
|
|
83
115
|
proveedorSistemas: CreateInvoiceInputSchema.shape.proveedorSistemas,
|
|
@@ -183,7 +215,14 @@ function registerCreateInvoiceTool(server) {
|
|
|
183
215
|
let xml = buildFacturaXml(factura);
|
|
184
216
|
const xmlValidation = await validateDocumentXml(xml);
|
|
185
217
|
if (!xmlValidation.valid) throw new Error(xmlValidation.issues.join("; "));
|
|
186
|
-
const allocatedSequence = await
|
|
218
|
+
const allocatedSequence = await nextIssuerSequence(
|
|
219
|
+
args.environment,
|
|
220
|
+
args.emisor.identificacion.tipo,
|
|
221
|
+
taxpayerId,
|
|
222
|
+
docTypeCode,
|
|
223
|
+
branch,
|
|
224
|
+
pos
|
|
225
|
+
);
|
|
187
226
|
clave = buildClave({
|
|
188
227
|
date: now,
|
|
189
228
|
taxpayerId,
|
|
@@ -214,7 +253,7 @@ function registerCreateInvoiceTool(server) {
|
|
|
214
253
|
]
|
|
215
254
|
};
|
|
216
255
|
} catch (error) {
|
|
217
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
256
|
+
const message = sanitizeLogString(error instanceof Error ? error.message : String(error));
|
|
218
257
|
return {
|
|
219
258
|
content: [
|
|
220
259
|
{
|
|
@@ -230,6 +269,7 @@ function registerCreateInvoiceTool(server) {
|
|
|
230
269
|
}
|
|
231
270
|
|
|
232
271
|
// src/tools/document-tools.ts
|
|
272
|
+
import { sanitizeLogString as sanitizeLogString2 } from "@dojocoding/hacienda-sdk";
|
|
233
273
|
import { z as z2 } from "zod";
|
|
234
274
|
import {
|
|
235
275
|
parseClave,
|
|
@@ -241,17 +281,33 @@ import {
|
|
|
241
281
|
import { DOCUMENT_TYPE_NAMES } from "@dojocoding/hacienda-shared";
|
|
242
282
|
|
|
243
283
|
// src/tools/api-client.ts
|
|
244
|
-
import {
|
|
284
|
+
import { createHash as createHash2 } from "crypto";
|
|
285
|
+
import { bootstrapClient, loadConfig } from "@dojocoding/hacienda-sdk";
|
|
245
286
|
var clientCache = /* @__PURE__ */ new Map();
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
287
|
+
function fingerprint(config) {
|
|
288
|
+
return createHash2("sha256").update(JSON.stringify([config.profileName, config.profile, config.password, config.p12Pin])).digest("hex");
|
|
289
|
+
}
|
|
290
|
+
async function createMcpApiClient(profileName = "default") {
|
|
291
|
+
const env = {
|
|
292
|
+
HACIENDA_USERNAME: process.env.HACIENDA_USERNAME,
|
|
293
|
+
HACIENDA_PASSWORD: process.env.HACIENDA_PASSWORD,
|
|
294
|
+
HACIENDA_P12_PIN: process.env.HACIENDA_P12_PIN
|
|
295
|
+
};
|
|
296
|
+
const config = await loadConfig(profileName, { env });
|
|
297
|
+
const identity = fingerprint(config);
|
|
298
|
+
const cached = clientCache.get(profileName);
|
|
299
|
+
if (cached?.fingerprint === identity) return cached.promise;
|
|
300
|
+
const promise = bootstrapClient({ profileName, configOptions: { env } }).then(({ httpClient, config: authenticatedConfig }) => {
|
|
301
|
+
if (clientCache.get(profileName) === entry)
|
|
302
|
+
entry.fingerprint = fingerprint(authenticatedConfig);
|
|
303
|
+
return httpClient;
|
|
304
|
+
}).catch((error) => {
|
|
305
|
+
if (clientCache.get(profileName) === entry) clientCache.delete(profileName);
|
|
306
|
+
throw error;
|
|
307
|
+
});
|
|
308
|
+
const entry = { fingerprint: identity, promise };
|
|
309
|
+
clientCache.set(profileName, entry);
|
|
310
|
+
return entry.promise;
|
|
255
311
|
}
|
|
256
312
|
|
|
257
313
|
// src/tools/document-tools.ts
|
|
@@ -294,7 +350,7 @@ function registerCheckStatusTool(server) {
|
|
|
294
350
|
]
|
|
295
351
|
};
|
|
296
352
|
} catch (error) {
|
|
297
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
353
|
+
const message = sanitizeLogString2(error instanceof Error ? error.message : String(error));
|
|
298
354
|
return {
|
|
299
355
|
content: [
|
|
300
356
|
{
|
|
@@ -360,7 +416,7 @@ function registerListDocumentsTool(server) {
|
|
|
360
416
|
]
|
|
361
417
|
};
|
|
362
418
|
} catch (error) {
|
|
363
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
419
|
+
const message = sanitizeLogString2(error instanceof Error ? error.message : String(error));
|
|
364
420
|
return {
|
|
365
421
|
content: [
|
|
366
422
|
{
|
|
@@ -426,7 +482,7 @@ ${xmlContent}` : null
|
|
|
426
482
|
]
|
|
427
483
|
};
|
|
428
484
|
} catch (error) {
|
|
429
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
485
|
+
const message = sanitizeLogString2(error instanceof Error ? error.message : String(error));
|
|
430
486
|
return {
|
|
431
487
|
content: [
|
|
432
488
|
{
|
|
@@ -443,7 +499,7 @@ ${xmlContent}` : null
|
|
|
443
499
|
|
|
444
500
|
// src/tools/lookup-tools.ts
|
|
445
501
|
import { z as z3 } from "zod";
|
|
446
|
-
import { lookupTaxpayer } from "@dojocoding/hacienda-sdk";
|
|
502
|
+
import { lookupTaxpayer, sanitizeLogString as sanitizeLogString3 } from "@dojocoding/hacienda-sdk";
|
|
447
503
|
import { IDENTIFICATION_TYPE_NAMES } from "@dojocoding/hacienda-shared";
|
|
448
504
|
function registerLookupTaxpayerTool(server) {
|
|
449
505
|
server.tool(
|
|
@@ -474,7 +530,7 @@ function registerLookupTaxpayerTool(server) {
|
|
|
474
530
|
]
|
|
475
531
|
};
|
|
476
532
|
} catch (error) {
|
|
477
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
533
|
+
const message = sanitizeLogString3(error instanceof Error ? error.message : String(error));
|
|
478
534
|
return {
|
|
479
535
|
content: [
|
|
480
536
|
{
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dojocoding/hacienda-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "MCP Server for Costa Rica electronic invoicing (AI-accessible tools)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"model-context-protocol"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@dojocoding/hacienda-sdk": "0.4.2",
|
|
32
|
+
"@dojocoding/hacienda-shared": "0.4.2",
|
|
31
33
|
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
32
34
|
"xmllint-wasm": "^5.3.0",
|
|
33
|
-
"zod": "^4.6.5"
|
|
34
|
-
"@dojocoding/hacienda-sdk": "0.4.0",
|
|
35
|
-
"@dojocoding/hacienda-shared": "0.4.0"
|
|
35
|
+
"zod": "^4.6.5"
|
|
36
36
|
},
|
|
37
37
|
"license": "MIT",
|
|
38
38
|
"author": "Dojo Coding Labs <dev@dojocoding.io> (https://dojocoding.io)",
|
|
@@ -57,4 +57,4 @@
|
|
|
57
57
|
"test:watch": "vitest",
|
|
58
58
|
"test:coverage": "vitest run --coverage"
|
|
59
59
|
}
|
|
60
|
-
}
|
|
60
|
+
}
|