@emisso/sii-api 0.1.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/LICENSE +21 -0
- package/dist/adapters/next.cjs +1035 -0
- package/dist/adapters/next.cjs.map +1 -0
- package/dist/adapters/next.d.cts +35 -0
- package/dist/adapters/next.d.ts +35 -0
- package/dist/adapters/next.js +1031 -0
- package/dist/adapters/next.js.map +1 -0
- package/dist/index.cjs +1043 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1224 -0
- package/dist/index.d.ts +1224 -0
- package/dist/index.js +1012 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc2) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc2 = __getOwnPropDesc(from, key)) || desc2.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/adapters/next.ts
|
|
31
|
+
var next_exports = {};
|
|
32
|
+
__export(next_exports, {
|
|
33
|
+
createSiiRouter: () => createSiiRouter
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(next_exports);
|
|
36
|
+
var import_postgres = __toESM(require("postgres"), 1);
|
|
37
|
+
var import_postgres_js = require("drizzle-orm/postgres-js");
|
|
38
|
+
|
|
39
|
+
// src/handlers/router.ts
|
|
40
|
+
function compilePattern(pattern) {
|
|
41
|
+
const paramNames = [];
|
|
42
|
+
const regexStr = pattern.split("/").map((segment) => {
|
|
43
|
+
if (segment.startsWith(":")) {
|
|
44
|
+
paramNames.push(segment.slice(1));
|
|
45
|
+
return "([^/]+)";
|
|
46
|
+
}
|
|
47
|
+
return segment;
|
|
48
|
+
}).join("/");
|
|
49
|
+
return { regex: new RegExp(`^${regexStr}$`), paramNames };
|
|
50
|
+
}
|
|
51
|
+
function createRouter(routes) {
|
|
52
|
+
const compiled = routes.map((r) => {
|
|
53
|
+
const { regex, paramNames } = compilePattern(r.pattern);
|
|
54
|
+
return { method: r.method, regex, paramNames, handler: r.handler };
|
|
55
|
+
});
|
|
56
|
+
return async (req, tenantId) => {
|
|
57
|
+
const url = new URL(req.url);
|
|
58
|
+
const method = req.method.toUpperCase();
|
|
59
|
+
const path = url.pathname;
|
|
60
|
+
for (const route of compiled) {
|
|
61
|
+
if (route.method !== method) continue;
|
|
62
|
+
const match = path.match(route.regex);
|
|
63
|
+
if (!match) continue;
|
|
64
|
+
const params = {};
|
|
65
|
+
route.paramNames.forEach((name, i) => {
|
|
66
|
+
params[name] = match[i + 1];
|
|
67
|
+
});
|
|
68
|
+
return route.handler(req, { tenantId, params });
|
|
69
|
+
}
|
|
70
|
+
return Response.json(
|
|
71
|
+
{ error: { _type: "NotFoundError", message: "Route not found" } },
|
|
72
|
+
{ status: 404 }
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/repos/credential-repo.ts
|
|
78
|
+
var import_effect3 = require("effect");
|
|
79
|
+
var import_drizzle_orm = require("drizzle-orm");
|
|
80
|
+
|
|
81
|
+
// src/db/schema/index.ts
|
|
82
|
+
var import_pg_core5 = require("drizzle-orm/pg-core");
|
|
83
|
+
|
|
84
|
+
// src/db/schema/credentials.ts
|
|
85
|
+
var import_pg_core = require("drizzle-orm/pg-core");
|
|
86
|
+
var credentials = siiSchema.table(
|
|
87
|
+
"credentials",
|
|
88
|
+
{
|
|
89
|
+
id: (0, import_pg_core.uuid)("id").defaultRandom().primaryKey(),
|
|
90
|
+
tenantId: (0, import_pg_core.uuid)("tenant_id").notNull(),
|
|
91
|
+
// NOTE: `env` is typed as SiiEnv at the application layer via Drizzle's $type<>().
|
|
92
|
+
// A DB-level CHECK constraint (env IN ('production','certification')) should be
|
|
93
|
+
// added via a future migration to enforce this at the database level as well.
|
|
94
|
+
env: (0, import_pg_core.text)("env").notNull().$type(),
|
|
95
|
+
// TODO: ENCRYPT AT REST — these fields store sensitive SII credentials.
|
|
96
|
+
// Must implement application-level encryption (AES-256-GCM) before production.
|
|
97
|
+
certBase64: (0, import_pg_core.text)("cert_base64"),
|
|
98
|
+
certPassword: (0, import_pg_core.text)("cert_password"),
|
|
99
|
+
portalRut: (0, import_pg_core.text)("portal_rut"),
|
|
100
|
+
// TODO: ENCRYPT AT REST — portal password stores sensitive SII credentials.
|
|
101
|
+
// Must implement application-level encryption (AES-256-GCM) before production.
|
|
102
|
+
portalPassword: (0, import_pg_core.text)("portal_password"),
|
|
103
|
+
createdAt: (0, import_pg_core.timestamp)("created_at").notNull().defaultNow(),
|
|
104
|
+
updatedAt: (0, import_pg_core.timestamp)("updated_at").notNull().defaultNow()
|
|
105
|
+
},
|
|
106
|
+
(table) => [(0, import_pg_core.unique)("uq_credentials_tenant_env").on(table.tenantId, table.env)]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// src/db/schema/token-cache.ts
|
|
110
|
+
var import_pg_core2 = require("drizzle-orm/pg-core");
|
|
111
|
+
var tokenCache = siiSchema.table(
|
|
112
|
+
"token_cache",
|
|
113
|
+
{
|
|
114
|
+
id: (0, import_pg_core2.uuid)("id").defaultRandom().primaryKey(),
|
|
115
|
+
credentialId: (0, import_pg_core2.uuid)("credential_id").notNull().references(() => credentials.id, { onDelete: "cascade" }),
|
|
116
|
+
tokenType: (0, import_pg_core2.text)("token_type").notNull().$type(),
|
|
117
|
+
tokenValue: (0, import_pg_core2.text)("token_value").notNull(),
|
|
118
|
+
expiresAt: (0, import_pg_core2.timestamp)("expires_at", { withTimezone: true }).notNull(),
|
|
119
|
+
createdAt: (0, import_pg_core2.timestamp)("created_at").notNull().defaultNow()
|
|
120
|
+
},
|
|
121
|
+
(table) => [(0, import_pg_core2.unique)("uq_token_cache_cred_type").on(table.credentialId, table.tokenType)]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// src/db/schema/invoices.ts
|
|
125
|
+
var import_pg_core3 = require("drizzle-orm/pg-core");
|
|
126
|
+
var invoices = siiSchema.table(
|
|
127
|
+
"invoices",
|
|
128
|
+
{
|
|
129
|
+
id: (0, import_pg_core3.uuid)("id").defaultRandom().primaryKey(),
|
|
130
|
+
tenantId: (0, import_pg_core3.uuid)("tenant_id").notNull(),
|
|
131
|
+
documentType: (0, import_pg_core3.text)("document_type").notNull().$type(),
|
|
132
|
+
number: (0, import_pg_core3.integer)("number").notNull(),
|
|
133
|
+
issuerRut: (0, import_pg_core3.text)("issuer_rut"),
|
|
134
|
+
issuerName: (0, import_pg_core3.text)("issuer_name"),
|
|
135
|
+
receiverRut: (0, import_pg_core3.text)("receiver_rut"),
|
|
136
|
+
receiverName: (0, import_pg_core3.text)("receiver_name"),
|
|
137
|
+
date: (0, import_pg_core3.date)("date"),
|
|
138
|
+
netAmount: (0, import_pg_core3.numeric)("net_amount", { precision: 16, scale: 0 }),
|
|
139
|
+
exemptAmount: (0, import_pg_core3.numeric)("exempt_amount", { precision: 16, scale: 0 }),
|
|
140
|
+
vatAmount: (0, import_pg_core3.numeric)("vat_amount", { precision: 16, scale: 0 }),
|
|
141
|
+
totalAmount: (0, import_pg_core3.numeric)("total_amount", { precision: 16, scale: 0 }),
|
|
142
|
+
taxPeriodYear: (0, import_pg_core3.integer)("tax_period_year").notNull(),
|
|
143
|
+
taxPeriodMonth: (0, import_pg_core3.integer)("tax_period_month").notNull(),
|
|
144
|
+
issueType: (0, import_pg_core3.text)("issue_type").notNull().$type(),
|
|
145
|
+
confirmationStatus: (0, import_pg_core3.text)("confirmation_status").$type(),
|
|
146
|
+
raw: (0, import_pg_core3.jsonb)("raw").$type(),
|
|
147
|
+
createdAt: (0, import_pg_core3.timestamp)("created_at").notNull().defaultNow(),
|
|
148
|
+
updatedAt: (0, import_pg_core3.timestamp)("updated_at").notNull().defaultNow()
|
|
149
|
+
},
|
|
150
|
+
(table) => [
|
|
151
|
+
(0, import_pg_core3.unique)("uq_invoices_natural_key").on(
|
|
152
|
+
table.tenantId,
|
|
153
|
+
table.documentType,
|
|
154
|
+
table.number,
|
|
155
|
+
table.issuerRut,
|
|
156
|
+
table.receiverRut,
|
|
157
|
+
table.issueType
|
|
158
|
+
),
|
|
159
|
+
(0, import_pg_core3.index)("idx_invoices_tenant_period").on(
|
|
160
|
+
table.tenantId,
|
|
161
|
+
table.taxPeriodYear,
|
|
162
|
+
table.taxPeriodMonth
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// src/db/schema/sync-jobs.ts
|
|
168
|
+
var import_pg_core4 = require("drizzle-orm/pg-core");
|
|
169
|
+
var syncJobs = siiSchema.table("sync_jobs", {
|
|
170
|
+
id: (0, import_pg_core4.uuid)("id").defaultRandom().primaryKey(),
|
|
171
|
+
tenantId: (0, import_pg_core4.uuid)("tenant_id").notNull(),
|
|
172
|
+
operation: (0, import_pg_core4.text)("operation").notNull().$type(),
|
|
173
|
+
periodYear: (0, import_pg_core4.integer)("period_year").notNull(),
|
|
174
|
+
periodMonth: (0, import_pg_core4.integer)("period_month").notNull(),
|
|
175
|
+
issueType: (0, import_pg_core4.text)("issue_type").notNull().$type(),
|
|
176
|
+
status: (0, import_pg_core4.text)("status").notNull().default("pending").$type(),
|
|
177
|
+
startedAt: (0, import_pg_core4.timestamp)("started_at"),
|
|
178
|
+
completedAt: (0, import_pg_core4.timestamp)("completed_at"),
|
|
179
|
+
recordsFetched: (0, import_pg_core4.integer)("records_fetched"),
|
|
180
|
+
errorMessage: (0, import_pg_core4.text)("error_message"),
|
|
181
|
+
createdAt: (0, import_pg_core4.timestamp)("created_at").notNull().defaultNow()
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// src/db/schema/index.ts
|
|
185
|
+
var siiSchema = (0, import_pg_core5.pgSchema)("sii");
|
|
186
|
+
|
|
187
|
+
// src/core/effect/app-error.ts
|
|
188
|
+
var import_effect = require("effect");
|
|
189
|
+
var NotFoundError = class _NotFoundError extends import_effect.Data.TaggedError("NotFoundError") {
|
|
190
|
+
static make(entity, entityId) {
|
|
191
|
+
return new _NotFoundError({
|
|
192
|
+
message: `${entity} not found${entityId ? `: ${entityId}` : ""}`,
|
|
193
|
+
entity,
|
|
194
|
+
entityId
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
var ValidationError = class _ValidationError extends import_effect.Data.TaggedError("ValidationError") {
|
|
199
|
+
static make(message, field, details) {
|
|
200
|
+
return new _ValidationError({
|
|
201
|
+
message,
|
|
202
|
+
field,
|
|
203
|
+
details
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
static fromZodErrors(message, issues) {
|
|
207
|
+
return new _ValidationError({
|
|
208
|
+
message,
|
|
209
|
+
fieldErrors: issues.map((i) => ({
|
|
210
|
+
path: i.path.join("."),
|
|
211
|
+
message: i.message
|
|
212
|
+
}))
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var ForbiddenError = class _ForbiddenError extends import_effect.Data.TaggedError("ForbiddenError") {
|
|
217
|
+
static make(message = "Forbidden", requiredPermission) {
|
|
218
|
+
return new _ForbiddenError({
|
|
219
|
+
message,
|
|
220
|
+
requiredPermission
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var DbError = class _DbError extends import_effect.Data.TaggedError("DbError") {
|
|
225
|
+
static make(operation, cause) {
|
|
226
|
+
const message = cause instanceof Error ? cause.message : "Database operation failed";
|
|
227
|
+
return new _DbError({
|
|
228
|
+
message,
|
|
229
|
+
operation,
|
|
230
|
+
cause
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
var ConflictError = class _ConflictError extends import_effect.Data.TaggedError("ConflictError") {
|
|
235
|
+
static make(message, resource, conflictingValue) {
|
|
236
|
+
return new _ConflictError({
|
|
237
|
+
message,
|
|
238
|
+
resource,
|
|
239
|
+
conflictingValue
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
var SiiAuthError = class _SiiAuthError extends import_effect.Data.TaggedError("SiiAuthError") {
|
|
244
|
+
static make(message, cause) {
|
|
245
|
+
return new _SiiAuthError({
|
|
246
|
+
message,
|
|
247
|
+
cause
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
function serializeAppError(error) {
|
|
252
|
+
const result = {
|
|
253
|
+
_type: error._tag,
|
|
254
|
+
message: error.message
|
|
255
|
+
};
|
|
256
|
+
switch (error._tag) {
|
|
257
|
+
case "NotFoundError":
|
|
258
|
+
if (error.entity) result.entity = error.entity;
|
|
259
|
+
if (error.entityId) result.entityId = error.entityId;
|
|
260
|
+
break;
|
|
261
|
+
case "ValidationError":
|
|
262
|
+
if (error.field) result.field = error.field;
|
|
263
|
+
if (error.details) result.details = error.details;
|
|
264
|
+
if (error.fieldErrors) result.fieldErrors = error.fieldErrors;
|
|
265
|
+
break;
|
|
266
|
+
case "ForbiddenError":
|
|
267
|
+
if (error.requiredPermission)
|
|
268
|
+
result.requiredPermission = error.requiredPermission;
|
|
269
|
+
break;
|
|
270
|
+
case "DbError":
|
|
271
|
+
if (error.operation) result.operation = error.operation;
|
|
272
|
+
break;
|
|
273
|
+
case "ConflictError":
|
|
274
|
+
if (error.resource) result.resource = error.resource;
|
|
275
|
+
if (error.conflictingValue)
|
|
276
|
+
result.conflictingValue = error.conflictingValue;
|
|
277
|
+
break;
|
|
278
|
+
case "SiiAuthError":
|
|
279
|
+
break;
|
|
280
|
+
default: {
|
|
281
|
+
const _exhaustive = error;
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/core/effect/repo-helpers.ts
|
|
289
|
+
var import_effect2 = require("effect");
|
|
290
|
+
function queryOneOrFail(operation, entity, entityId, query) {
|
|
291
|
+
return import_effect2.Effect.tryPromise({
|
|
292
|
+
try: query,
|
|
293
|
+
catch: (e) => DbError.make(operation, e)
|
|
294
|
+
}).pipe(
|
|
295
|
+
import_effect2.Effect.flatMap(
|
|
296
|
+
(row) => row ? import_effect2.Effect.succeed(row) : import_effect2.Effect.fail(NotFoundError.make(entity, entityId))
|
|
297
|
+
)
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/repos/credential-repo.ts
|
|
302
|
+
function createCredentialRepo(db) {
|
|
303
|
+
return {
|
|
304
|
+
getByTenantAndEnv(tenantId, env) {
|
|
305
|
+
return queryOneOrFail(
|
|
306
|
+
"credential.getByTenantAndEnv",
|
|
307
|
+
"Credential",
|
|
308
|
+
`${tenantId}/${env}`,
|
|
309
|
+
() => db.select().from(credentials).where(
|
|
310
|
+
(0, import_drizzle_orm.and)(
|
|
311
|
+
(0, import_drizzle_orm.eq)(credentials.tenantId, tenantId),
|
|
312
|
+
(0, import_drizzle_orm.eq)(credentials.env, env)
|
|
313
|
+
)
|
|
314
|
+
).then((rows) => rows[0])
|
|
315
|
+
);
|
|
316
|
+
},
|
|
317
|
+
upsert(tenantId, data) {
|
|
318
|
+
return import_effect3.Effect.tryPromise({
|
|
319
|
+
try: () => db.insert(credentials).values({ ...data, tenantId }).onConflictDoUpdate({
|
|
320
|
+
target: [credentials.tenantId, credentials.env],
|
|
321
|
+
set: { ...data, updatedAt: /* @__PURE__ */ new Date() }
|
|
322
|
+
}).returning().then((rows) => rows[0]),
|
|
323
|
+
catch: (e) => DbError.make("credential.upsert", e)
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
delete(tenantId, env) {
|
|
327
|
+
return import_effect3.Effect.tryPromise({
|
|
328
|
+
try: () => db.delete(credentials).where(
|
|
329
|
+
(0, import_drizzle_orm.and)(
|
|
330
|
+
(0, import_drizzle_orm.eq)(credentials.tenantId, tenantId),
|
|
331
|
+
(0, import_drizzle_orm.eq)(credentials.env, env)
|
|
332
|
+
)
|
|
333
|
+
).returning().then((rows) => rows.length > 0),
|
|
334
|
+
catch: (e) => DbError.make("credential.delete", e)
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/repos/token-cache-repo.ts
|
|
341
|
+
var import_effect4 = require("effect");
|
|
342
|
+
var import_drizzle_orm2 = require("drizzle-orm");
|
|
343
|
+
function createTokenCacheRepo(db) {
|
|
344
|
+
return {
|
|
345
|
+
get(credentialId, tokenType) {
|
|
346
|
+
return import_effect4.Effect.tryPromise({
|
|
347
|
+
try: () => db.select().from(tokenCache).where(
|
|
348
|
+
(0, import_drizzle_orm2.and)(
|
|
349
|
+
(0, import_drizzle_orm2.eq)(tokenCache.credentialId, credentialId),
|
|
350
|
+
(0, import_drizzle_orm2.eq)(tokenCache.tokenType, tokenType)
|
|
351
|
+
)
|
|
352
|
+
).then((rows) => rows[0] ?? null),
|
|
353
|
+
catch: (e) => DbError.make("tokenCache.get", e)
|
|
354
|
+
});
|
|
355
|
+
},
|
|
356
|
+
upsert(credentialId, tokenType, tokenValue, expiresAt) {
|
|
357
|
+
return import_effect4.Effect.tryPromise({
|
|
358
|
+
try: () => db.insert(tokenCache).values({ credentialId, tokenType, tokenValue, expiresAt }).onConflictDoUpdate({
|
|
359
|
+
target: [tokenCache.credentialId, tokenCache.tokenType],
|
|
360
|
+
set: { tokenValue, expiresAt, createdAt: /* @__PURE__ */ new Date() }
|
|
361
|
+
}).returning().then((rows) => rows[0]),
|
|
362
|
+
catch: (e) => DbError.make("tokenCache.upsert", e)
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
deleteByCredential(credentialId) {
|
|
366
|
+
return import_effect4.Effect.tryPromise({
|
|
367
|
+
try: () => db.delete(tokenCache).where((0, import_drizzle_orm2.eq)(tokenCache.credentialId, credentialId)).then(() => void 0),
|
|
368
|
+
catch: (e) => DbError.make("tokenCache.deleteByCredential", e)
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/repos/invoice-repo.ts
|
|
375
|
+
var import_effect5 = require("effect");
|
|
376
|
+
var import_drizzle_orm3 = require("drizzle-orm");
|
|
377
|
+
function createInvoiceRepo(db) {
|
|
378
|
+
return {
|
|
379
|
+
list(tenantId, filters) {
|
|
380
|
+
return import_effect5.Effect.tryPromise({
|
|
381
|
+
try: () => {
|
|
382
|
+
const conditions = [(0, import_drizzle_orm3.eq)(invoices.tenantId, tenantId)];
|
|
383
|
+
if (filters?.periodYear !== void 0) {
|
|
384
|
+
conditions.push((0, import_drizzle_orm3.eq)(invoices.taxPeriodYear, filters.periodYear));
|
|
385
|
+
}
|
|
386
|
+
if (filters?.periodMonth !== void 0) {
|
|
387
|
+
conditions.push((0, import_drizzle_orm3.eq)(invoices.taxPeriodMonth, filters.periodMonth));
|
|
388
|
+
}
|
|
389
|
+
if (filters?.issueType) {
|
|
390
|
+
conditions.push((0, import_drizzle_orm3.eq)(invoices.issueType, filters.issueType));
|
|
391
|
+
}
|
|
392
|
+
if (filters?.documentType) {
|
|
393
|
+
conditions.push((0, import_drizzle_orm3.eq)(invoices.documentType, filters.documentType));
|
|
394
|
+
}
|
|
395
|
+
return db.select().from(invoices).where((0, import_drizzle_orm3.and)(...conditions)).limit(filters?.limit ?? 100).offset(filters?.offset ?? 0);
|
|
396
|
+
},
|
|
397
|
+
catch: (e) => DbError.make("invoice.list", e)
|
|
398
|
+
});
|
|
399
|
+
},
|
|
400
|
+
upsertMany(rows) {
|
|
401
|
+
if (rows.length === 0) return import_effect5.Effect.succeed(0);
|
|
402
|
+
return import_effect5.Effect.tryPromise({
|
|
403
|
+
try: () => db.insert(invoices).values(rows).onConflictDoUpdate({
|
|
404
|
+
target: [
|
|
405
|
+
invoices.tenantId,
|
|
406
|
+
invoices.documentType,
|
|
407
|
+
invoices.number,
|
|
408
|
+
invoices.issuerRut,
|
|
409
|
+
invoices.receiverRut,
|
|
410
|
+
invoices.issueType
|
|
411
|
+
],
|
|
412
|
+
set: {
|
|
413
|
+
issuerName: import_drizzle_orm3.sql`excluded.issuer_name`,
|
|
414
|
+
receiverName: import_drizzle_orm3.sql`excluded.receiver_name`,
|
|
415
|
+
date: import_drizzle_orm3.sql`excluded.date`,
|
|
416
|
+
netAmount: import_drizzle_orm3.sql`excluded.net_amount`,
|
|
417
|
+
exemptAmount: import_drizzle_orm3.sql`excluded.exempt_amount`,
|
|
418
|
+
vatAmount: import_drizzle_orm3.sql`excluded.vat_amount`,
|
|
419
|
+
totalAmount: import_drizzle_orm3.sql`excluded.total_amount`,
|
|
420
|
+
confirmationStatus: import_drizzle_orm3.sql`excluded.confirmation_status`,
|
|
421
|
+
raw: import_drizzle_orm3.sql`excluded.raw`,
|
|
422
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
423
|
+
}
|
|
424
|
+
}).returning({ id: invoices.id }).then((rows2) => rows2.length),
|
|
425
|
+
catch: (e) => DbError.make("invoice.upsertMany", e)
|
|
426
|
+
});
|
|
427
|
+
},
|
|
428
|
+
count(tenantId, filters) {
|
|
429
|
+
return import_effect5.Effect.tryPromise({
|
|
430
|
+
try: () => {
|
|
431
|
+
const conditions = [(0, import_drizzle_orm3.eq)(invoices.tenantId, tenantId)];
|
|
432
|
+
if (filters?.periodYear !== void 0) {
|
|
433
|
+
conditions.push((0, import_drizzle_orm3.eq)(invoices.taxPeriodYear, filters.periodYear));
|
|
434
|
+
}
|
|
435
|
+
if (filters?.periodMonth !== void 0) {
|
|
436
|
+
conditions.push((0, import_drizzle_orm3.eq)(invoices.taxPeriodMonth, filters.periodMonth));
|
|
437
|
+
}
|
|
438
|
+
if (filters?.issueType) {
|
|
439
|
+
conditions.push((0, import_drizzle_orm3.eq)(invoices.issueType, filters.issueType));
|
|
440
|
+
}
|
|
441
|
+
return db.select({ count: import_drizzle_orm3.sql`count(*)::int` }).from(invoices).where((0, import_drizzle_orm3.and)(...conditions)).then((rows) => rows[0]?.count ?? 0);
|
|
442
|
+
},
|
|
443
|
+
catch: (e) => DbError.make("invoice.count", e)
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/repos/sync-job-repo.ts
|
|
450
|
+
var import_effect6 = require("effect");
|
|
451
|
+
var import_drizzle_orm4 = require("drizzle-orm");
|
|
452
|
+
function createSyncJobRepo(db) {
|
|
453
|
+
return {
|
|
454
|
+
create(data) {
|
|
455
|
+
return import_effect6.Effect.tryPromise({
|
|
456
|
+
try: () => db.insert(syncJobs).values(data).returning().then((rows) => rows[0]),
|
|
457
|
+
catch: (e) => DbError.make("syncJob.create", e)
|
|
458
|
+
});
|
|
459
|
+
},
|
|
460
|
+
getById(id) {
|
|
461
|
+
return queryOneOrFail(
|
|
462
|
+
"syncJob.getById",
|
|
463
|
+
"SyncJob",
|
|
464
|
+
id,
|
|
465
|
+
() => db.select().from(syncJobs).where((0, import_drizzle_orm4.eq)(syncJobs.id, id)).then((rows) => rows[0])
|
|
466
|
+
);
|
|
467
|
+
},
|
|
468
|
+
listByTenant(tenantId, filters) {
|
|
469
|
+
return import_effect6.Effect.tryPromise({
|
|
470
|
+
try: () => {
|
|
471
|
+
const conditions = [(0, import_drizzle_orm4.eq)(syncJobs.tenantId, tenantId)];
|
|
472
|
+
if (filters?.periodYear !== void 0) {
|
|
473
|
+
conditions.push((0, import_drizzle_orm4.eq)(syncJobs.periodYear, filters.periodYear));
|
|
474
|
+
}
|
|
475
|
+
if (filters?.periodMonth !== void 0) {
|
|
476
|
+
conditions.push((0, import_drizzle_orm4.eq)(syncJobs.periodMonth, filters.periodMonth));
|
|
477
|
+
}
|
|
478
|
+
if (filters?.issueType) {
|
|
479
|
+
conditions.push((0, import_drizzle_orm4.eq)(syncJobs.issueType, filters.issueType));
|
|
480
|
+
}
|
|
481
|
+
return db.select().from(syncJobs).where((0, import_drizzle_orm4.and)(...conditions)).orderBy((0, import_drizzle_orm4.desc)(syncJobs.createdAt)).limit(20);
|
|
482
|
+
},
|
|
483
|
+
catch: (e) => DbError.make("syncJob.listByTenant", e)
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
update(id, data) {
|
|
487
|
+
return queryOneOrFail(
|
|
488
|
+
"syncJob.update",
|
|
489
|
+
"SyncJob",
|
|
490
|
+
id,
|
|
491
|
+
() => db.update(syncJobs).set(data).where((0, import_drizzle_orm4.eq)(syncJobs.id, id)).returning().then((rows) => rows[0])
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/services/credential-service.ts
|
|
498
|
+
var import_effect7 = require("effect");
|
|
499
|
+
function createCredentialService(deps) {
|
|
500
|
+
const { credentialRepo, tokenCacheRepo } = deps;
|
|
501
|
+
const enc = (v) => v && deps.encrypt ? deps.encrypt(v) : v;
|
|
502
|
+
const dec = (v) => v && deps.decrypt ? deps.decrypt(v) : v;
|
|
503
|
+
return {
|
|
504
|
+
save(tenantId, input) {
|
|
505
|
+
return credentialRepo.upsert(tenantId, {
|
|
506
|
+
env: input.env,
|
|
507
|
+
certBase64: enc(input.certBase64) ?? null,
|
|
508
|
+
certPassword: enc(input.certPassword) ?? null,
|
|
509
|
+
portalRut: input.portalRut ?? null,
|
|
510
|
+
portalPassword: enc(input.portalPassword) ?? null
|
|
511
|
+
});
|
|
512
|
+
},
|
|
513
|
+
getStatus(tenantId, env) {
|
|
514
|
+
return credentialRepo.getByTenantAndEnv(tenantId, env).pipe(
|
|
515
|
+
import_effect7.Effect.map((cred) => ({
|
|
516
|
+
connected: true,
|
|
517
|
+
env: cred.env,
|
|
518
|
+
hasCert: !!cred.certBase64,
|
|
519
|
+
hasPortal: !!cred.portalRut && !!cred.portalPassword,
|
|
520
|
+
portalRut: cred.portalRut
|
|
521
|
+
})),
|
|
522
|
+
import_effect7.Effect.catchTag(
|
|
523
|
+
"NotFoundError",
|
|
524
|
+
() => import_effect7.Effect.succeed({
|
|
525
|
+
connected: false,
|
|
526
|
+
env,
|
|
527
|
+
hasCert: false,
|
|
528
|
+
hasPortal: false,
|
|
529
|
+
portalRut: null
|
|
530
|
+
})
|
|
531
|
+
)
|
|
532
|
+
);
|
|
533
|
+
},
|
|
534
|
+
disconnect(tenantId, env) {
|
|
535
|
+
return import_effect7.Effect.gen(function* () {
|
|
536
|
+
const cred = yield* credentialRepo.getByTenantAndEnv(tenantId, env);
|
|
537
|
+
yield* tokenCacheRepo.deleteByCredential(cred.id);
|
|
538
|
+
yield* credentialRepo.delete(tenantId, env);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/services/auth-service.ts
|
|
545
|
+
var import_effect8 = require("effect");
|
|
546
|
+
var import_sii = require("@emisso/sii");
|
|
547
|
+
function toMessage(e) {
|
|
548
|
+
return e instanceof Error ? e.message : String(e);
|
|
549
|
+
}
|
|
550
|
+
async function authenticateSoap(certBase64, certPassword, env) {
|
|
551
|
+
const certData = (0, import_sii.loadCertFromBase64)(certBase64, certPassword);
|
|
552
|
+
const seed = await (0, import_sii.getSeed)({ certPath: "", certPassword: "", env });
|
|
553
|
+
const signedSeed = (0, import_sii.signSeedFromCertData)(seed, certData);
|
|
554
|
+
return (0, import_sii.getToken)(signedSeed, { certPath: "", certPassword: "", env });
|
|
555
|
+
}
|
|
556
|
+
function createAuthService(deps) {
|
|
557
|
+
const { credentialRepo, tokenCacheRepo } = deps;
|
|
558
|
+
const dec = (v) => deps.decrypt ? deps.decrypt(v) : v;
|
|
559
|
+
return {
|
|
560
|
+
/**
|
|
561
|
+
* Get a valid SOAP token, using cache if available.
|
|
562
|
+
*/
|
|
563
|
+
getSoapToken(tenantId, env) {
|
|
564
|
+
return import_effect8.Effect.gen(function* () {
|
|
565
|
+
const cred = yield* credentialRepo.getByTenantAndEnv(tenantId, env);
|
|
566
|
+
if (!cred.certBase64 || !cred.certPassword) {
|
|
567
|
+
return yield* import_effect8.Effect.fail(
|
|
568
|
+
SiiAuthError.make("No certificate configured for SOAP authentication")
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
const cached = yield* tokenCacheRepo.get(cred.id, "soap");
|
|
572
|
+
if (cached && new Date(cached.expiresAt).getTime() > Date.now()) {
|
|
573
|
+
return { token: cached.tokenValue, expiresAt: new Date(cached.expiresAt) };
|
|
574
|
+
}
|
|
575
|
+
const siiToken = yield* import_effect8.Effect.tryPromise({
|
|
576
|
+
try: () => authenticateSoap(dec(cred.certBase64), dec(cred.certPassword), env),
|
|
577
|
+
catch: (e) => SiiAuthError.make(`SOAP authentication failed: ${toMessage(e)}`, e)
|
|
578
|
+
});
|
|
579
|
+
yield* tokenCacheRepo.upsert(
|
|
580
|
+
cred.id,
|
|
581
|
+
"soap",
|
|
582
|
+
siiToken.token,
|
|
583
|
+
siiToken.expiresAt
|
|
584
|
+
);
|
|
585
|
+
return siiToken;
|
|
586
|
+
});
|
|
587
|
+
},
|
|
588
|
+
/**
|
|
589
|
+
* Get a portal session via fresh login.
|
|
590
|
+
*/
|
|
591
|
+
getPortalSession(tenantId, env) {
|
|
592
|
+
return import_effect8.Effect.gen(function* () {
|
|
593
|
+
const cred = yield* credentialRepo.getByTenantAndEnv(tenantId, env);
|
|
594
|
+
if (!cred.portalRut || !cred.portalPassword) {
|
|
595
|
+
return yield* import_effect8.Effect.fail(
|
|
596
|
+
SiiAuthError.make("No portal credentials configured")
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
const session = yield* import_effect8.Effect.tryPromise({
|
|
600
|
+
try: () => (0, import_sii.portalLogin)(
|
|
601
|
+
{ rut: cred.portalRut, claveTributaria: dec(cred.portalPassword), env },
|
|
602
|
+
{ connectBrowser: deps.connectBrowser }
|
|
603
|
+
),
|
|
604
|
+
catch: (e) => SiiAuthError.make(`Portal login failed: ${toMessage(e)}`, e)
|
|
605
|
+
});
|
|
606
|
+
return session;
|
|
607
|
+
});
|
|
608
|
+
},
|
|
609
|
+
/**
|
|
610
|
+
* Test credentials by attempting authentication.
|
|
611
|
+
* Runs SOAP and portal tests concurrently.
|
|
612
|
+
*/
|
|
613
|
+
testConnection(tenantId, env) {
|
|
614
|
+
return import_effect8.Effect.gen(function* () {
|
|
615
|
+
const cred = yield* credentialRepo.getByTenantAndEnv(tenantId, env);
|
|
616
|
+
const errors = [];
|
|
617
|
+
const soapTest = cred.certBase64 && cred.certPassword ? import_effect8.Effect.tryPromise({
|
|
618
|
+
try: () => authenticateSoap(dec(cred.certBase64), dec(cred.certPassword), env).then(() => true),
|
|
619
|
+
catch: (e) => toMessage(e)
|
|
620
|
+
}).pipe(import_effect8.Effect.catchAll((msg) => {
|
|
621
|
+
errors.push(`SOAP: ${msg}`);
|
|
622
|
+
return import_effect8.Effect.succeed(false);
|
|
623
|
+
})) : import_effect8.Effect.sync(() => {
|
|
624
|
+
errors.push("SOAP: No certificate configured");
|
|
625
|
+
return false;
|
|
626
|
+
});
|
|
627
|
+
const portalTest = cred.portalRut && cred.portalPassword ? import_effect8.Effect.tryPromise({
|
|
628
|
+
try: () => (0, import_sii.portalLogin)(
|
|
629
|
+
{ rut: cred.portalRut, claveTributaria: dec(cred.portalPassword), env },
|
|
630
|
+
{ connectBrowser: deps.connectBrowser }
|
|
631
|
+
).then(() => true),
|
|
632
|
+
catch: (e) => toMessage(e)
|
|
633
|
+
}).pipe(import_effect8.Effect.catchAll((msg) => {
|
|
634
|
+
errors.push(`Portal: ${msg}`);
|
|
635
|
+
return import_effect8.Effect.succeed(false);
|
|
636
|
+
})) : import_effect8.Effect.sync(() => {
|
|
637
|
+
errors.push("Portal: No portal credentials configured");
|
|
638
|
+
return false;
|
|
639
|
+
});
|
|
640
|
+
const [soapOk, portalOk] = yield* import_effect8.Effect.all(
|
|
641
|
+
[soapTest, portalTest],
|
|
642
|
+
{ concurrency: 2 }
|
|
643
|
+
);
|
|
644
|
+
return { soap: soapOk, portal: portalOk, errors };
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/services/invoice-service.ts
|
|
651
|
+
var import_effect9 = require("effect");
|
|
652
|
+
var import_sii2 = require("@emisso/sii");
|
|
653
|
+
|
|
654
|
+
// src/core/bridge.ts
|
|
655
|
+
function invoiceToRow(tenantId, invoice, issueType) {
|
|
656
|
+
return {
|
|
657
|
+
tenantId,
|
|
658
|
+
documentType: invoice.documentType,
|
|
659
|
+
number: invoice.number,
|
|
660
|
+
issuerRut: invoice.issuer.rut || null,
|
|
661
|
+
issuerName: invoice.issuer.name || null,
|
|
662
|
+
receiverRut: invoice.receiver.rut || null,
|
|
663
|
+
receiverName: invoice.receiver.name || null,
|
|
664
|
+
date: invoice.date || null,
|
|
665
|
+
netAmount: String(invoice.netAmount),
|
|
666
|
+
exemptAmount: String(invoice.exemptAmount),
|
|
667
|
+
vatAmount: String(invoice.vatAmount),
|
|
668
|
+
totalAmount: String(invoice.totalAmount),
|
|
669
|
+
taxPeriodYear: invoice.taxPeriod.year,
|
|
670
|
+
taxPeriodMonth: invoice.taxPeriod.month,
|
|
671
|
+
issueType,
|
|
672
|
+
confirmationStatus: invoice.confirmationStatus ?? null,
|
|
673
|
+
raw: invoice.raw ?? null
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function rowToInvoice(row) {
|
|
677
|
+
return {
|
|
678
|
+
id: `${row.documentType}-${row.number}-${row.issuerRut || row.receiverRut || ""}`,
|
|
679
|
+
number: row.number,
|
|
680
|
+
issuer: {
|
|
681
|
+
rut: row.issuerRut ?? "",
|
|
682
|
+
name: row.issuerName ?? ""
|
|
683
|
+
},
|
|
684
|
+
receiver: {
|
|
685
|
+
rut: row.receiverRut ?? "",
|
|
686
|
+
name: row.receiverName ?? ""
|
|
687
|
+
},
|
|
688
|
+
date: row.date ?? "",
|
|
689
|
+
netAmount: Number(row.netAmount ?? 0),
|
|
690
|
+
exemptAmount: Number(row.exemptAmount ?? 0),
|
|
691
|
+
vatAmount: Number(row.vatAmount ?? 0),
|
|
692
|
+
totalAmount: Number(row.totalAmount ?? 0),
|
|
693
|
+
currency: "CLP",
|
|
694
|
+
taxPeriod: {
|
|
695
|
+
year: row.taxPeriodYear,
|
|
696
|
+
month: row.taxPeriodMonth
|
|
697
|
+
},
|
|
698
|
+
documentType: row.documentType,
|
|
699
|
+
confirmationStatus: row.confirmationStatus ?? "REGISTRO",
|
|
700
|
+
raw: row.raw ?? {}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/services/invoice-service.ts
|
|
705
|
+
function createInvoiceService(deps) {
|
|
706
|
+
const { invoiceRepo, syncJobRepo, authService, credentialRepo } = deps;
|
|
707
|
+
return {
|
|
708
|
+
/**
|
|
709
|
+
* Trigger an RCV invoice sync for a given period.
|
|
710
|
+
* Creates a sync job, fetches from SII, upserts into DB.
|
|
711
|
+
*/
|
|
712
|
+
sync(tenantId, env, periodYear, periodMonth, issueType) {
|
|
713
|
+
return import_effect9.Effect.gen(function* () {
|
|
714
|
+
const job = yield* syncJobRepo.create({
|
|
715
|
+
tenantId,
|
|
716
|
+
operation: "rcv_sync",
|
|
717
|
+
periodYear,
|
|
718
|
+
periodMonth,
|
|
719
|
+
issueType,
|
|
720
|
+
status: "running",
|
|
721
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
722
|
+
});
|
|
723
|
+
const failJob = (err) => syncJobRepo.update(job.id, {
|
|
724
|
+
status: "failed",
|
|
725
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
726
|
+
errorMessage: err.message
|
|
727
|
+
}).pipe(import_effect9.Effect.flatMap(() => import_effect9.Effect.fail(err)));
|
|
728
|
+
const cred = yield* credentialRepo.getByTenantAndEnv(tenantId, env);
|
|
729
|
+
if (!cred.portalRut) {
|
|
730
|
+
const err = SiiAuthError.make("No portal RUT configured for RCV sync");
|
|
731
|
+
return yield* failJob(err);
|
|
732
|
+
}
|
|
733
|
+
const session = yield* authService.getPortalSession(tenantId, env).pipe(import_effect9.Effect.catchAll(failJob));
|
|
734
|
+
const siiInvoices = yield* import_effect9.Effect.tryPromise({
|
|
735
|
+
try: () => (0, import_sii2.listInvoices)(session, {
|
|
736
|
+
rut: cred.portalRut,
|
|
737
|
+
issueType,
|
|
738
|
+
period: { year: periodYear, month: periodMonth }
|
|
739
|
+
}),
|
|
740
|
+
catch: (e) => SiiAuthError.make(
|
|
741
|
+
`RCV fetch failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
742
|
+
e
|
|
743
|
+
)
|
|
744
|
+
}).pipe(import_effect9.Effect.catchAll(failJob));
|
|
745
|
+
const rows = siiInvoices.map(
|
|
746
|
+
(inv) => invoiceToRow(tenantId, inv, issueType)
|
|
747
|
+
);
|
|
748
|
+
const count = yield* invoiceRepo.upsertMany(rows);
|
|
749
|
+
return yield* syncJobRepo.update(job.id, {
|
|
750
|
+
status: "completed",
|
|
751
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
752
|
+
recordsFetched: count
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/handlers/auth-handlers.ts
|
|
760
|
+
var import_effect11 = require("effect");
|
|
761
|
+
|
|
762
|
+
// src/handlers/handler-utils.ts
|
|
763
|
+
function resolveEnv(req) {
|
|
764
|
+
const url = new URL(req.url);
|
|
765
|
+
const env = url.searchParams.get("env");
|
|
766
|
+
return env === "certification" ? "certification" : "production";
|
|
767
|
+
}
|
|
768
|
+
function parsePeriod(period) {
|
|
769
|
+
const [y, m] = period.split("-");
|
|
770
|
+
return { year: parseInt(y, 10), month: parseInt(m, 10) };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/validation/schemas.ts
|
|
774
|
+
var import_zod = require("zod");
|
|
775
|
+
var import_sii3 = require("@emisso/sii");
|
|
776
|
+
var SaveCredentialsSchema = import_zod.z.object({
|
|
777
|
+
env: import_sii3.SiiEnvSchema.default("production"),
|
|
778
|
+
certBase64: import_zod.z.string().optional(),
|
|
779
|
+
certPassword: import_zod.z.string().optional(),
|
|
780
|
+
portalRut: import_zod.z.string().optional(),
|
|
781
|
+
portalPassword: import_zod.z.string().optional()
|
|
782
|
+
}).refine(
|
|
783
|
+
(d) => d.certBase64 || d.certPassword || (d.portalRut || d.portalPassword),
|
|
784
|
+
{ message: "At least one credential pair is required: certificate (certBase64 + certPassword) or portal (portalRut + portalPassword)" }
|
|
785
|
+
).refine(
|
|
786
|
+
(d) => !d.certBase64 && !d.certPassword || d.certBase64 && d.certPassword,
|
|
787
|
+
{ message: "certBase64 and certPassword must both be provided or both omitted" }
|
|
788
|
+
).refine(
|
|
789
|
+
(d) => !d.portalRut && !d.portalPassword || d.portalRut && d.portalPassword,
|
|
790
|
+
{ message: "portalRut and portalPassword must both be provided or both omitted" }
|
|
791
|
+
);
|
|
792
|
+
var SyncInvoicesSchema = import_zod.z.object({
|
|
793
|
+
period: import_zod.z.string().regex(/^\d{4}-\d{2}$/, "Period must be YYYY-MM format"),
|
|
794
|
+
type: import_sii3.IssueTypeSchema.default("received")
|
|
795
|
+
});
|
|
796
|
+
var ListInvoicesQuerySchema = import_zod.z.object({
|
|
797
|
+
period: import_zod.z.string().regex(/^\d{4}-\d{2}$/).optional(),
|
|
798
|
+
type: import_sii3.IssueTypeSchema.optional(),
|
|
799
|
+
documentType: import_sii3.DteTypeSchema.optional(),
|
|
800
|
+
limit: import_zod.z.coerce.number().int().positive().max(1e3).default(100),
|
|
801
|
+
offset: import_zod.z.coerce.number().int().nonnegative().default(0)
|
|
802
|
+
});
|
|
803
|
+
var SyncStatusQuerySchema = import_zod.z.object({
|
|
804
|
+
period: import_zod.z.string().regex(/^\d{4}-\d{2}$/).optional(),
|
|
805
|
+
type: import_sii3.IssueTypeSchema.optional()
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// src/core/effect/http-response.ts
|
|
809
|
+
var import_effect10 = require("effect");
|
|
810
|
+
var STATUS_MAP = {
|
|
811
|
+
ValidationError: 400,
|
|
812
|
+
ForbiddenError: 403,
|
|
813
|
+
NotFoundError: 404,
|
|
814
|
+
ConflictError: 409,
|
|
815
|
+
SiiAuthError: 502,
|
|
816
|
+
DbError: 500
|
|
817
|
+
};
|
|
818
|
+
function toErrorResponse(error) {
|
|
819
|
+
const status = STATUS_MAP[error._tag] ?? 500;
|
|
820
|
+
const body = serializeAppError(error);
|
|
821
|
+
return Response.json({ error: body }, { status });
|
|
822
|
+
}
|
|
823
|
+
function handleEffect(effect, toResponse = jsonResponse) {
|
|
824
|
+
return import_effect10.Effect.runPromise(
|
|
825
|
+
effect.pipe(
|
|
826
|
+
import_effect10.Effect.map(toResponse),
|
|
827
|
+
import_effect10.Effect.catchAll((err) => import_effect10.Effect.succeed(toErrorResponse(err)))
|
|
828
|
+
)
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
function jsonResponse(data, status = 200) {
|
|
832
|
+
return Response.json(data, { status });
|
|
833
|
+
}
|
|
834
|
+
function createdResponse(data) {
|
|
835
|
+
return Response.json(data, { status: 201 });
|
|
836
|
+
}
|
|
837
|
+
function noContentResponse() {
|
|
838
|
+
return new Response(null, { status: 204 });
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/handlers/auth-handlers.ts
|
|
842
|
+
function createAuthHandlers(deps) {
|
|
843
|
+
const { credentialService, authService } = deps;
|
|
844
|
+
const saveCredentials = (req, ctx) => handleEffect(
|
|
845
|
+
import_effect11.Effect.gen(function* () {
|
|
846
|
+
const body = yield* import_effect11.Effect.tryPromise({
|
|
847
|
+
try: () => req.json(),
|
|
848
|
+
catch: () => ValidationError.make("Invalid JSON body")
|
|
849
|
+
});
|
|
850
|
+
const parsed = SaveCredentialsSchema.safeParse(body);
|
|
851
|
+
if (!parsed.success) {
|
|
852
|
+
return yield* import_effect11.Effect.fail(
|
|
853
|
+
ValidationError.fromZodErrors("Invalid credentials data", parsed.error.issues)
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
const result = yield* credentialService.save(ctx.tenantId, parsed.data);
|
|
857
|
+
return {
|
|
858
|
+
id: result.id,
|
|
859
|
+
env: result.env,
|
|
860
|
+
hasCert: !!result.certBase64,
|
|
861
|
+
hasPortal: !!result.portalRut,
|
|
862
|
+
portalRut: result.portalRut,
|
|
863
|
+
createdAt: result.createdAt,
|
|
864
|
+
updatedAt: result.updatedAt
|
|
865
|
+
};
|
|
866
|
+
})
|
|
867
|
+
);
|
|
868
|
+
const getStatus = (req, ctx) => {
|
|
869
|
+
const env = resolveEnv(req);
|
|
870
|
+
return handleEffect(credentialService.getStatus(ctx.tenantId, env));
|
|
871
|
+
};
|
|
872
|
+
const testConnection = (req, ctx) => {
|
|
873
|
+
const env = resolveEnv(req);
|
|
874
|
+
return handleEffect(authService.testConnection(ctx.tenantId, env));
|
|
875
|
+
};
|
|
876
|
+
const disconnect = (req, ctx) => {
|
|
877
|
+
const env = resolveEnv(req);
|
|
878
|
+
return handleEffect(
|
|
879
|
+
credentialService.disconnect(ctx.tenantId, env).pipe(import_effect11.Effect.map(() => null)),
|
|
880
|
+
() => noContentResponse()
|
|
881
|
+
);
|
|
882
|
+
};
|
|
883
|
+
return {
|
|
884
|
+
saveCredentials,
|
|
885
|
+
getStatus,
|
|
886
|
+
testConnection,
|
|
887
|
+
disconnect
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/handlers/invoice-handlers.ts
|
|
892
|
+
var import_effect12 = require("effect");
|
|
893
|
+
function createInvoiceHandlers(deps) {
|
|
894
|
+
const { invoiceService, invoiceRepo, syncJobRepo } = deps;
|
|
895
|
+
const listInvoices2 = (req, ctx) => handleEffect(
|
|
896
|
+
import_effect12.Effect.gen(function* () {
|
|
897
|
+
const url = new URL(req.url);
|
|
898
|
+
const query = ListInvoicesQuerySchema.safeParse(
|
|
899
|
+
Object.fromEntries(url.searchParams)
|
|
900
|
+
);
|
|
901
|
+
if (!query.success) {
|
|
902
|
+
return yield* import_effect12.Effect.fail(
|
|
903
|
+
ValidationError.fromZodErrors("Invalid query parameters", query.error.issues)
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
const { period, type, documentType, limit, offset } = query.data;
|
|
907
|
+
const periodParsed = period ? parsePeriod(period) : void 0;
|
|
908
|
+
const rows = yield* invoiceRepo.list(ctx.tenantId, {
|
|
909
|
+
periodYear: periodParsed?.year,
|
|
910
|
+
periodMonth: periodParsed?.month,
|
|
911
|
+
issueType: type,
|
|
912
|
+
documentType,
|
|
913
|
+
limit,
|
|
914
|
+
offset
|
|
915
|
+
});
|
|
916
|
+
return { data: rows.map(rowToInvoice), count: rows.length };
|
|
917
|
+
})
|
|
918
|
+
);
|
|
919
|
+
const syncInvoices = (req, ctx) => handleEffect(
|
|
920
|
+
import_effect12.Effect.gen(function* () {
|
|
921
|
+
const body = yield* import_effect12.Effect.tryPromise({
|
|
922
|
+
try: () => req.json(),
|
|
923
|
+
catch: () => ValidationError.make("Invalid JSON body")
|
|
924
|
+
});
|
|
925
|
+
const parsed = SyncInvoicesSchema.safeParse(body);
|
|
926
|
+
if (!parsed.success) {
|
|
927
|
+
return yield* import_effect12.Effect.fail(
|
|
928
|
+
ValidationError.fromZodErrors("Invalid sync parameters", parsed.error.issues)
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
const { year: periodYear, month: periodMonth } = parsePeriod(parsed.data.period);
|
|
932
|
+
const env = resolveEnv(req);
|
|
933
|
+
return yield* invoiceService.sync(ctx.tenantId, env, periodYear, periodMonth, parsed.data.type);
|
|
934
|
+
}),
|
|
935
|
+
createdResponse
|
|
936
|
+
);
|
|
937
|
+
const getSyncStatus = (req, ctx) => handleEffect(
|
|
938
|
+
import_effect12.Effect.gen(function* () {
|
|
939
|
+
const url = new URL(req.url);
|
|
940
|
+
const query = SyncStatusQuerySchema.safeParse(
|
|
941
|
+
Object.fromEntries(url.searchParams)
|
|
942
|
+
);
|
|
943
|
+
if (!query.success) {
|
|
944
|
+
return yield* import_effect12.Effect.fail(
|
|
945
|
+
ValidationError.fromZodErrors("Invalid query parameters", query.error.issues)
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
const { period, type } = query.data;
|
|
949
|
+
const periodParsed = period ? parsePeriod(period) : void 0;
|
|
950
|
+
const jobs = yield* syncJobRepo.listByTenant(ctx.tenantId, {
|
|
951
|
+
periodYear: periodParsed?.year,
|
|
952
|
+
periodMonth: periodParsed?.month,
|
|
953
|
+
issueType: type
|
|
954
|
+
});
|
|
955
|
+
return { data: jobs };
|
|
956
|
+
})
|
|
957
|
+
);
|
|
958
|
+
return {
|
|
959
|
+
listInvoices: listInvoices2,
|
|
960
|
+
syncInvoices,
|
|
961
|
+
getSyncStatus
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/adapters/next.ts
|
|
966
|
+
function createSiiRouter(config) {
|
|
967
|
+
if (!!config.encrypt !== !!config.decrypt) {
|
|
968
|
+
throw new Error("SiiRouterConfig: encrypt and decrypt must be provided together");
|
|
969
|
+
}
|
|
970
|
+
const sql2 = (0, import_postgres.default)(config.databaseUrl);
|
|
971
|
+
const db = (0, import_postgres_js.drizzle)(sql2);
|
|
972
|
+
const credentialRepo = createCredentialRepo(db);
|
|
973
|
+
const tokenCacheRepo = createTokenCacheRepo(db);
|
|
974
|
+
const invoiceRepo = createInvoiceRepo(db);
|
|
975
|
+
const syncJobRepo = createSyncJobRepo(db);
|
|
976
|
+
const credentialService = createCredentialService({
|
|
977
|
+
credentialRepo,
|
|
978
|
+
tokenCacheRepo,
|
|
979
|
+
encrypt: config.encrypt,
|
|
980
|
+
decrypt: config.decrypt
|
|
981
|
+
});
|
|
982
|
+
const authService = createAuthService({
|
|
983
|
+
credentialRepo,
|
|
984
|
+
tokenCacheRepo,
|
|
985
|
+
decrypt: config.decrypt,
|
|
986
|
+
connectBrowser: config.connectBrowser
|
|
987
|
+
});
|
|
988
|
+
const invoiceService = createInvoiceService({
|
|
989
|
+
invoiceRepo,
|
|
990
|
+
syncJobRepo,
|
|
991
|
+
authService,
|
|
992
|
+
credentialRepo
|
|
993
|
+
});
|
|
994
|
+
const authHandlers = createAuthHandlers({ credentialService, authService });
|
|
995
|
+
const invoiceHandlers = createInvoiceHandlers({
|
|
996
|
+
invoiceService,
|
|
997
|
+
invoiceRepo,
|
|
998
|
+
syncJobRepo
|
|
999
|
+
});
|
|
1000
|
+
const base = config.basePath;
|
|
1001
|
+
const routes = [
|
|
1002
|
+
// Auth / Credentials
|
|
1003
|
+
{ method: "PUT", pattern: `${base}/auth`, handler: authHandlers.saveCredentials },
|
|
1004
|
+
{ method: "GET", pattern: `${base}/auth`, handler: authHandlers.getStatus },
|
|
1005
|
+
{ method: "POST", pattern: `${base}/auth/test`, handler: authHandlers.testConnection },
|
|
1006
|
+
{ method: "DELETE", pattern: `${base}/auth`, handler: authHandlers.disconnect },
|
|
1007
|
+
// Invoices
|
|
1008
|
+
{ method: "GET", pattern: `${base}/invoices`, handler: invoiceHandlers.listInvoices },
|
|
1009
|
+
{ method: "POST", pattern: `${base}/invoices/sync`, handler: invoiceHandlers.syncInvoices },
|
|
1010
|
+
{ method: "GET", pattern: `${base}/invoices/sync`, handler: invoiceHandlers.getSyncStatus }
|
|
1011
|
+
];
|
|
1012
|
+
const router = createRouter(routes);
|
|
1013
|
+
const resolveTenantId = config.resolveTenantId ?? ((req) => req.headers.get("X-Tenant-Id"));
|
|
1014
|
+
async function handle(req) {
|
|
1015
|
+
const tenantId = await resolveTenantId(req);
|
|
1016
|
+
if (!tenantId) {
|
|
1017
|
+
return Response.json(
|
|
1018
|
+
{ error: { _type: "ForbiddenError", message: "Missing tenant ID" } },
|
|
1019
|
+
{ status: 403 }
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
return router(req, tenantId);
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
GET: handle,
|
|
1026
|
+
POST: handle,
|
|
1027
|
+
PUT: handle,
|
|
1028
|
+
DELETE: handle
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1032
|
+
0 && (module.exports = {
|
|
1033
|
+
createSiiRouter
|
|
1034
|
+
});
|
|
1035
|
+
//# sourceMappingURL=next.cjs.map
|