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