@open-mercato/tillio 0.7.1-develop.7176.1.d229567a50
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/.turbo/turbo-build.log +2 -0
- package/build.mjs +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/tillio/__integration__/TC-PHONE-001-pull-readiness.spec.js +17 -0
- package/dist/modules/tillio/__integration__/TC-PHONE-001-pull-readiness.spec.js.map +7 -0
- package/dist/modules/tillio/__integration__/TC-PHONE-002-pull-blocked.spec.js +27 -0
- package/dist/modules/tillio/__integration__/TC-PHONE-002-pull-blocked.spec.js.map +7 -0
- package/dist/modules/tillio/__integration__/TC-PHONE-003-operators.spec.js +47 -0
- package/dist/modules/tillio/__integration__/TC-PHONE-003-operators.spec.js.map +7 -0
- package/dist/modules/tillio/__integration__/TC-PHONE-004-pull-widget.spec.js +11 -0
- package/dist/modules/tillio/__integration__/TC-PHONE-004-pull-widget.spec.js.map +7 -0
- package/dist/modules/tillio/__integration__/TC-PHONE-005-operators-widget.spec.js +13 -0
- package/dist/modules/tillio/__integration__/TC-PHONE-005-operators-widget.spec.js.map +7 -0
- package/dist/modules/tillio/acl.js +9 -0
- package/dist/modules/tillio/acl.js.map +7 -0
- package/dist/modules/tillio/api/operators/[id]/route.js +78 -0
- package/dist/modules/tillio/api/operators/[id]/route.js.map +7 -0
- package/dist/modules/tillio/api/operators/route.js +171 -0
- package/dist/modules/tillio/api/operators/route.js.map +7 -0
- package/dist/modules/tillio/api/pull/route.js +180 -0
- package/dist/modules/tillio/api/pull/route.js.map +7 -0
- package/dist/modules/tillio/cli.js +108 -0
- package/dist/modules/tillio/cli.js.map +7 -0
- package/dist/modules/tillio/di.js +16 -0
- package/dist/modules/tillio/di.js.map +7 -0
- package/dist/modules/tillio/index.js +12 -0
- package/dist/modules/tillio/index.js.map +7 -0
- package/dist/modules/tillio/integration.js +62 -0
- package/dist/modules/tillio/integration.js.map +7 -0
- package/dist/modules/tillio/lib/adapter.js +107 -0
- package/dist/modules/tillio/lib/adapter.js.map +7 -0
- package/dist/modules/tillio/lib/client.js +249 -0
- package/dist/modules/tillio/lib/client.js.map +7 -0
- package/dist/modules/tillio/lib/environment.js +34 -0
- package/dist/modules/tillio/lib/environment.js.map +7 -0
- package/dist/modules/tillio/lib/error-codes.js +59 -0
- package/dist/modules/tillio/lib/error-codes.js.map +7 -0
- package/dist/modules/tillio/lib/errors.js +12 -0
- package/dist/modules/tillio/lib/errors.js.map +7 -0
- package/dist/modules/tillio/lib/health.js +41 -0
- package/dist/modules/tillio/lib/health.js.map +7 -0
- package/dist/modules/tillio/lib/locking.js +18 -0
- package/dist/modules/tillio/lib/locking.js.map +7 -0
- package/dist/modules/tillio/lib/normalizer.js +134 -0
- package/dist/modules/tillio/lib/normalizer.js.map +7 -0
- package/dist/modules/tillio/lib/operators-store.js +62 -0
- package/dist/modules/tillio/lib/operators-store.js.map +7 -0
- package/dist/modules/tillio/lib/operators.js +168 -0
- package/dist/modules/tillio/lib/operators.js.map +7 -0
- package/dist/modules/tillio/lib/preset.js +122 -0
- package/dist/modules/tillio/lib/preset.js.map +7 -0
- package/dist/modules/tillio/lib/pull-job.js +265 -0
- package/dist/modules/tillio/lib/pull-job.js.map +7 -0
- package/dist/modules/tillio/lib/pull-readiness.js +36 -0
- package/dist/modules/tillio/lib/pull-readiness.js.map +7 -0
- package/dist/modules/tillio/lib/queue.js +22 -0
- package/dist/modules/tillio/lib/queue.js.map +7 -0
- package/dist/modules/tillio/lib/tz.js +85 -0
- package/dist/modules/tillio/lib/tz.js.map +7 -0
- package/dist/modules/tillio/lib/url-guard.js +24 -0
- package/dist/modules/tillio/lib/url-guard.js.map +7 -0
- package/dist/modules/tillio/setup.js +45 -0
- package/dist/modules/tillio/setup.js.map +7 -0
- package/dist/modules/tillio/widgets/injection/operators-config/widget.client.js +208 -0
- package/dist/modules/tillio/widgets/injection/operators-config/widget.client.js.map +7 -0
- package/dist/modules/tillio/widgets/injection/operators-config/widget.js +17 -0
- package/dist/modules/tillio/widgets/injection/operators-config/widget.js.map +7 -0
- package/dist/modules/tillio/widgets/injection/pull-calls/widget.client.js +148 -0
- package/dist/modules/tillio/widgets/injection/pull-calls/widget.client.js.map +7 -0
- package/dist/modules/tillio/widgets/injection/pull-calls/widget.js +17 -0
- package/dist/modules/tillio/widgets/injection/pull-calls/widget.js.map +7 -0
- package/dist/modules/tillio/widgets/injection-table.js +21 -0
- package/dist/modules/tillio/widgets/injection-table.js.map +7 -0
- package/dist/modules/tillio/workers/tillio-pull.js +17 -0
- package/dist/modules/tillio/workers/tillio-pull.js.map +7 -0
- package/jest.config.cjs +35 -0
- package/package.json +92 -0
- package/src/index.ts +1 -0
- package/src/modules/tillio/__integration__/TC-PHONE-001-pull-readiness.spec.ts +38 -0
- package/src/modules/tillio/__integration__/TC-PHONE-002-pull-blocked.spec.ts +44 -0
- package/src/modules/tillio/__integration__/TC-PHONE-003-operators.spec.ts +76 -0
- package/src/modules/tillio/__integration__/TC-PHONE-004-pull-widget.spec.ts +22 -0
- package/src/modules/tillio/__integration__/TC-PHONE-005-operators-widget.spec.ts +27 -0
- package/src/modules/tillio/__tests__/adapter-fetch-calls.test.ts +148 -0
- package/src/modules/tillio/__tests__/client.test.ts +203 -0
- package/src/modules/tillio/__tests__/health.test.ts +122 -0
- package/src/modules/tillio/__tests__/normalizer.test.ts +143 -0
- package/src/modules/tillio/__tests__/operators-routes.test.ts +100 -0
- package/src/modules/tillio/__tests__/operators-store.test.ts +104 -0
- package/src/modules/tillio/__tests__/operators.test.ts +250 -0
- package/src/modules/tillio/__tests__/preset.test.ts +337 -0
- package/src/modules/tillio/__tests__/pull-body.test.ts +43 -0
- package/src/modules/tillio/__tests__/pull-job.test.ts +250 -0
- package/src/modules/tillio/__tests__/pull-readiness.test.ts +143 -0
- package/src/modules/tillio/__tests__/pull-route.test.ts +148 -0
- package/src/modules/tillio/__tests__/tz.test.ts +53 -0
- package/src/modules/tillio/__tests__/url-guard.test.ts +46 -0
- package/src/modules/tillio/acl.ts +5 -0
- package/src/modules/tillio/api/operators/[id]/route.ts +90 -0
- package/src/modules/tillio/api/operators/route.ts +191 -0
- package/src/modules/tillio/api/pull/route.ts +213 -0
- package/src/modules/tillio/cli.ts +125 -0
- package/src/modules/tillio/di.ts +16 -0
- package/src/modules/tillio/i18n/de.json +73 -0
- package/src/modules/tillio/i18n/en.json +73 -0
- package/src/modules/tillio/i18n/es.json +73 -0
- package/src/modules/tillio/i18n/ko.json +73 -0
- package/src/modules/tillio/i18n/pl.json +73 -0
- package/src/modules/tillio/index.ts +8 -0
- package/src/modules/tillio/integration.ts +58 -0
- package/src/modules/tillio/lib/adapter.ts +135 -0
- package/src/modules/tillio/lib/client.ts +329 -0
- package/src/modules/tillio/lib/environment.ts +53 -0
- package/src/modules/tillio/lib/error-codes.ts +67 -0
- package/src/modules/tillio/lib/errors.ts +11 -0
- package/src/modules/tillio/lib/health.ts +57 -0
- package/src/modules/tillio/lib/locking.ts +30 -0
- package/src/modules/tillio/lib/normalizer.ts +184 -0
- package/src/modules/tillio/lib/operators-store.ts +77 -0
- package/src/modules/tillio/lib/operators.ts +269 -0
- package/src/modules/tillio/lib/preset.ts +226 -0
- package/src/modules/tillio/lib/pull-job.ts +390 -0
- package/src/modules/tillio/lib/pull-readiness.ts +77 -0
- package/src/modules/tillio/lib/queue.ts +33 -0
- package/src/modules/tillio/lib/tz.ts +121 -0
- package/src/modules/tillio/lib/url-guard.ts +34 -0
- package/src/modules/tillio/setup.ts +49 -0
- package/src/modules/tillio/widgets/injection/operators-config/widget.client.tsx +304 -0
- package/src/modules/tillio/widgets/injection/operators-config/widget.ts +16 -0
- package/src/modules/tillio/widgets/injection/pull-calls/widget.client.tsx +214 -0
- package/src/modules/tillio/widgets/injection/pull-calls/widget.ts +16 -0
- package/src/modules/tillio/widgets/injection-table.ts +19 -0
- package/src/modules/tillio/workers/tillio-pull.ts +18 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
import {
|
|
3
|
+
PHONE_CALLS_CALL_INGEST_COMMAND_ID
|
|
4
|
+
} from "@open-mercato/core/modules/phone_calls/commands/calls";
|
|
5
|
+
import { emitPhoneCallsEvent } from "@open-mercato/core/modules/phone_calls/events";
|
|
6
|
+
import { TILLIO_INTEGRATION_ID, TILLIO_PROVIDER_KEY } from "../integration.js";
|
|
7
|
+
import { tillioAdapter } from "./adapter.js";
|
|
8
|
+
import { TillioApiError } from "./errors.js";
|
|
9
|
+
import {
|
|
10
|
+
classifyTillioError,
|
|
11
|
+
readTillioIntegrationState,
|
|
12
|
+
resolveEnvironment
|
|
13
|
+
} from "./operators.js";
|
|
14
|
+
import {
|
|
15
|
+
readOperatorsBlob
|
|
16
|
+
} from "./operators-store.js";
|
|
17
|
+
import { evaluatePullReadiness, PULL_BLOCKER_MESSAGES } from "./pull-readiness.js";
|
|
18
|
+
import { zonedDayEnd, zonedDayStart } from "./tz.js";
|
|
19
|
+
const logger = createLogger("tillio").child({ component: "pull-job" });
|
|
20
|
+
const TILLIO_PULL_JOB_TYPE = "tillio.calls.pull";
|
|
21
|
+
const MAX_BATCHES = 200;
|
|
22
|
+
async function resolvePullContext(credentialsService, em, scope) {
|
|
23
|
+
const environment = await resolveEnvironment(credentialsService, scope);
|
|
24
|
+
const integrationState = await readTillioIntegrationState(em, scope);
|
|
25
|
+
const blob = await readOperatorsBlob(credentialsService, scope);
|
|
26
|
+
const operator = blob.operators.find((entry) => entry.id === blob.defaultOperatorId) ?? blob.operators[0] ?? null;
|
|
27
|
+
return {
|
|
28
|
+
readiness: evaluatePullReadiness({
|
|
29
|
+
environment,
|
|
30
|
+
integrationEnabled: integrationState.enabled,
|
|
31
|
+
environmentHealthy: integrationState.healthy,
|
|
32
|
+
operator
|
|
33
|
+
}),
|
|
34
|
+
environment,
|
|
35
|
+
operator
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function buildIngestInput(call, scope) {
|
|
39
|
+
return {
|
|
40
|
+
organizationId: scope.organizationId,
|
|
41
|
+
tenantId: scope.tenantId,
|
|
42
|
+
providerKey: TILLIO_PROVIDER_KEY,
|
|
43
|
+
integrationId: TILLIO_INTEGRATION_ID,
|
|
44
|
+
externalCallId: call.externalCallId,
|
|
45
|
+
externalConversationId: call.externalConversationId ?? null,
|
|
46
|
+
direction: call.direction,
|
|
47
|
+
status: call.status,
|
|
48
|
+
participants: call.participants,
|
|
49
|
+
recording: call.recording ?? null,
|
|
50
|
+
startedAt: call.startedAt ?? null,
|
|
51
|
+
answeredAt: call.answeredAt ?? null,
|
|
52
|
+
endedAt: call.endedAt ?? null,
|
|
53
|
+
durationSeconds: call.durationSeconds ?? null,
|
|
54
|
+
providerFacts: call.providerFacts,
|
|
55
|
+
rawPayload: call.rawPayload
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function buildCommandContext(container, scope, userId) {
|
|
59
|
+
return {
|
|
60
|
+
container,
|
|
61
|
+
auth: userId ? { sub: userId, tenantId: scope.tenantId, orgId: scope.organizationId, isSuperAdmin: false } : null,
|
|
62
|
+
organizationScope: {
|
|
63
|
+
selectedId: scope.organizationId,
|
|
64
|
+
filterIds: [scope.organizationId],
|
|
65
|
+
allowedIds: [scope.organizationId],
|
|
66
|
+
tenantId: scope.tenantId
|
|
67
|
+
},
|
|
68
|
+
selectedOrganizationId: scope.organizationId,
|
|
69
|
+
organizationIds: [scope.organizationId]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function ingestBatch(calls, deps) {
|
|
73
|
+
const counts = { created: 0, updated: 0, failed: 0 };
|
|
74
|
+
for (const call of calls) {
|
|
75
|
+
try {
|
|
76
|
+
const executed = await deps.commandBus.execute(
|
|
77
|
+
PHONE_CALLS_CALL_INGEST_COMMAND_ID,
|
|
78
|
+
{ input: buildIngestInput(call, deps.scope), ctx: deps.commandContext }
|
|
79
|
+
);
|
|
80
|
+
if (executed.result.created) counts.created += 1;
|
|
81
|
+
else counts.updated += 1;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
counts.failed += 1;
|
|
84
|
+
logger.warn("ingest failed for a pulled call", { externalCallId: call.externalCallId, err });
|
|
85
|
+
await emitPhoneCallsEvent("phone_calls.call.ingest_failed", {
|
|
86
|
+
providerKey: TILLIO_PROVIDER_KEY,
|
|
87
|
+
externalCallId: call.externalCallId,
|
|
88
|
+
organizationId: deps.scope.organizationId,
|
|
89
|
+
tenantId: deps.scope.tenantId
|
|
90
|
+
}).catch(() => void 0);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return counts;
|
|
94
|
+
}
|
|
95
|
+
function describeFailure(err) {
|
|
96
|
+
if (err instanceof TillioApiError) {
|
|
97
|
+
return classifyTillioError(err) === "environment" ? { code: "provider_unreachable", message: "Tillio could not be reached or rejected the stored credentials." } : { code: "provider_error", message: "Tillio rejected the request for calls." };
|
|
98
|
+
}
|
|
99
|
+
return { code: "pull_failed", message: "The Tillio pull could not be completed." };
|
|
100
|
+
}
|
|
101
|
+
function describeTruncation(summary, anomaly) {
|
|
102
|
+
if (anomaly) {
|
|
103
|
+
return {
|
|
104
|
+
code: "pull_pagination_anomaly",
|
|
105
|
+
message: `Tillio returned an inconsistent page listing, so the sweep stopped early after ${summary.fetched} calls.`
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
code: "pull_truncated",
|
|
110
|
+
message: `Stopped after ${summary.batches} batches with more calls left in the range; ${summary.fetched} calls were ingested.`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function runTillioPullJob(params) {
|
|
114
|
+
const { container, payload } = params;
|
|
115
|
+
const adapter = params.adapter ?? tillioAdapter;
|
|
116
|
+
const scope = {
|
|
117
|
+
organizationId: payload.scope.organizationId,
|
|
118
|
+
tenantId: payload.scope.tenantId
|
|
119
|
+
};
|
|
120
|
+
const progressService = container.resolve("progressService");
|
|
121
|
+
const progressContext = {
|
|
122
|
+
tenantId: payload.scope.tenantId,
|
|
123
|
+
organizationId: payload.scope.organizationId,
|
|
124
|
+
userId: payload.scope.userId ?? null
|
|
125
|
+
};
|
|
126
|
+
const summary = {
|
|
127
|
+
fetched: 0,
|
|
128
|
+
created: 0,
|
|
129
|
+
updated: 0,
|
|
130
|
+
failed: 0,
|
|
131
|
+
batches: 0,
|
|
132
|
+
cancelled: false,
|
|
133
|
+
truncated: false,
|
|
134
|
+
resumeCursor: null
|
|
135
|
+
};
|
|
136
|
+
await progressService.startJob(payload.progressJobId, progressContext);
|
|
137
|
+
try {
|
|
138
|
+
const credentialsService = container.resolve("integrationCredentialsService");
|
|
139
|
+
const em = container.resolve("em");
|
|
140
|
+
const { readiness, environment, operator } = await resolvePullContext(credentialsService, em, scope);
|
|
141
|
+
if (readiness.blocker || !environment || !operator) {
|
|
142
|
+
const blocker = readiness.blocker ?? "environment_not_ready";
|
|
143
|
+
await progressService.failJob(
|
|
144
|
+
payload.progressJobId,
|
|
145
|
+
{
|
|
146
|
+
errorMessage: PULL_BLOCKER_MESSAGES[blocker],
|
|
147
|
+
resultSummary: { ...summary, code: blocker }
|
|
148
|
+
},
|
|
149
|
+
progressContext
|
|
150
|
+
);
|
|
151
|
+
return summary;
|
|
152
|
+
}
|
|
153
|
+
const deps = {
|
|
154
|
+
commandBus: container.resolve("commandBus"),
|
|
155
|
+
commandContext: buildCommandContext(container, scope, payload.scope.userId ?? null),
|
|
156
|
+
scope
|
|
157
|
+
};
|
|
158
|
+
const credentials = {
|
|
159
|
+
apiUrl: environment.apiUrl,
|
|
160
|
+
apiKey: environment.apiKey,
|
|
161
|
+
tenantSystemId: environment.tenantSystemId,
|
|
162
|
+
timeZone: environment.timeZone,
|
|
163
|
+
operator: {
|
|
164
|
+
id: operator.id,
|
|
165
|
+
plugin: operator.plugin,
|
|
166
|
+
token: operator.token,
|
|
167
|
+
tenantDomain: operator.tenantDomain
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const fetchBatch = (cursor2) => adapter.fetchCalls({
|
|
171
|
+
credentials,
|
|
172
|
+
scope,
|
|
173
|
+
integrationId: TILLIO_INTEGRATION_ID,
|
|
174
|
+
from: zonedDayStart(payload.from, environment.timeZone),
|
|
175
|
+
to: zonedDayEnd(payload.to, environment.timeZone),
|
|
176
|
+
cursor: cursor2,
|
|
177
|
+
limit: payload.limit ?? null
|
|
178
|
+
});
|
|
179
|
+
let cursor = payload.cursor ?? null;
|
|
180
|
+
let anomaly = null;
|
|
181
|
+
while (summary.batches < MAX_BATCHES) {
|
|
182
|
+
const cancelled = await progressService.isCancellationRequested(
|
|
183
|
+
payload.progressJobId,
|
|
184
|
+
payload.scope.tenantId,
|
|
185
|
+
payload.scope.organizationId
|
|
186
|
+
);
|
|
187
|
+
if (cancelled) {
|
|
188
|
+
summary.cancelled = true;
|
|
189
|
+
await progressService.markCancelled(payload.progressJobId, progressContext);
|
|
190
|
+
return summary;
|
|
191
|
+
}
|
|
192
|
+
const batch = await fetchBatch(cursor);
|
|
193
|
+
const counts = await ingestBatch(batch.calls, deps);
|
|
194
|
+
summary.batches += 1;
|
|
195
|
+
summary.fetched += batch.calls.length;
|
|
196
|
+
summary.created += counts.created;
|
|
197
|
+
summary.updated += counts.updated;
|
|
198
|
+
summary.failed += counts.failed;
|
|
199
|
+
await progressService.updateProgress(
|
|
200
|
+
payload.progressJobId,
|
|
201
|
+
{ processedCount: summary.fetched },
|
|
202
|
+
progressContext
|
|
203
|
+
);
|
|
204
|
+
if (batch.anomaly) {
|
|
205
|
+
anomaly = batch.anomaly;
|
|
206
|
+
summary.resumeCursor = batch.anomalyCursor ?? cursor;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
cursor = batch.nextCursor ?? null;
|
|
210
|
+
if (!cursor) break;
|
|
211
|
+
}
|
|
212
|
+
if (!anomaly && cursor) summary.resumeCursor = cursor;
|
|
213
|
+
if (anomaly || summary.resumeCursor) {
|
|
214
|
+
summary.truncated = true;
|
|
215
|
+
const failure = describeTruncation(summary, anomaly);
|
|
216
|
+
logger.warn("the Tillio pull did not cover the whole range", {
|
|
217
|
+
progressJobId: payload.progressJobId,
|
|
218
|
+
code: failure.code,
|
|
219
|
+
anomaly,
|
|
220
|
+
resumeCursor: summary.resumeCursor
|
|
221
|
+
});
|
|
222
|
+
await progressService.failJob(
|
|
223
|
+
payload.progressJobId,
|
|
224
|
+
{
|
|
225
|
+
errorMessage: failure.message,
|
|
226
|
+
resultSummary: { ...summary, code: failure.code, ...anomaly ? { anomaly } : {} }
|
|
227
|
+
},
|
|
228
|
+
progressContext
|
|
229
|
+
);
|
|
230
|
+
return summary;
|
|
231
|
+
}
|
|
232
|
+
await progressService.completeJob(
|
|
233
|
+
payload.progressJobId,
|
|
234
|
+
{ resultSummary: { ...summary } },
|
|
235
|
+
progressContext
|
|
236
|
+
);
|
|
237
|
+
return summary;
|
|
238
|
+
} catch (err) {
|
|
239
|
+
const failure = describeFailure(err);
|
|
240
|
+
logger.error("the Tillio pull failed", { progressJobId: payload.progressJobId, code: failure.code, err });
|
|
241
|
+
try {
|
|
242
|
+
await progressService.failJob(
|
|
243
|
+
payload.progressJobId,
|
|
244
|
+
{
|
|
245
|
+
errorMessage: failure.message,
|
|
246
|
+
errorStack: err instanceof Error ? err.stack?.slice(0, 1e4) : void 0,
|
|
247
|
+
resultSummary: { ...summary, code: failure.code }
|
|
248
|
+
},
|
|
249
|
+
progressContext
|
|
250
|
+
);
|
|
251
|
+
} catch (failErr) {
|
|
252
|
+
logger.error("failed to mark the pull progress job as failed", {
|
|
253
|
+
progressJobId: payload.progressJobId,
|
|
254
|
+
err: failErr
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export {
|
|
261
|
+
TILLIO_PULL_JOB_TYPE,
|
|
262
|
+
resolvePullContext,
|
|
263
|
+
runTillioPullJob
|
|
264
|
+
};
|
|
265
|
+
//# sourceMappingURL=pull-job.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/tillio/lib/pull-job.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'\nimport type {\n NormalizedPhoneCall,\n NormalizedPhoneCallBatch,\n PhoneCallBatchAnomaly,\n} from '@open-mercato/shared/modules/phone_calls/types'\nimport type { PhoneCallProviderAdapter } from '@open-mercato/shared/modules/phone_calls/provider'\nimport type {\n ProgressService,\n ProgressServiceContext,\n} from '@open-mercato/core/modules/progress/lib/progressService'\nimport {\n PHONE_CALLS_CALL_INGEST_COMMAND_ID,\n type IngestPhoneCallResult,\n} from '@open-mercato/core/modules/phone_calls/commands/calls'\nimport { emitPhoneCallsEvent } from '@open-mercato/core/modules/phone_calls/events'\nimport { TILLIO_INTEGRATION_ID, TILLIO_PROVIDER_KEY } from '../integration'\nimport { tillioAdapter } from './adapter'\nimport { TillioApiError } from './errors'\nimport {\n classifyTillioError,\n readTillioIntegrationState,\n resolveEnvironment,\n type TillioResolvedEnvironment,\n} from './operators'\nimport {\n readOperatorsBlob,\n type TillioCredentialsService,\n type TillioOperatorRecord,\n} from './operators-store'\nimport { evaluatePullReadiness, PULL_BLOCKER_MESSAGES, type PullReadiness } from './pull-readiness'\nimport { zonedDayEnd, zonedDayStart } from './tz'\n\nconst logger = createLogger('tillio').child({ component: 'pull-job' })\n\nexport const TILLIO_PULL_JOB_TYPE = 'tillio.calls.pull'\n\n// Only trips when the provider keeps advertising further pages, which would spin forever.\nconst MAX_BATCHES = 200\n\nexport type TillioPullScope = {\n tenantId: string\n organizationId: string\n userId?: string | null\n}\n\n// No operator token and no API key in the payload: the worker resolves credentials on every\n// attempt, so a job replayed after a rotation uses the current ones instead of stale copies.\nexport type TillioPullJobPayload = {\n progressJobId: string\n scope: TillioPullScope\n from: string\n to: string\n cursor?: string | null\n limit?: number | null\n}\n\nexport type TillioPullSummary = {\n fetched: number\n created: number\n updated: number\n failed: number\n batches: number\n cancelled: boolean\n /** True when the sweep stopped before the range was exhausted. */\n truncated: boolean\n /** Where a follow-up pull should pick up. Null unless `truncated`. */\n resumeCursor: string | null\n}\n\nexport type TillioPullContext = {\n readiness: PullReadiness\n environment: TillioResolvedEnvironment | null\n operator: TillioOperatorRecord | null\n}\n\ntype IngestDeps = {\n commandBus: CommandBus\n commandContext: CommandRuntimeContext\n scope: IntegrationScope\n}\n\ntype BatchCounts = Pick<TillioPullSummary, 'created' | 'updated' | 'failed'>\n\nexport async function resolvePullContext(\n credentialsService: TillioCredentialsService,\n em: EntityManager,\n scope: IntegrationScope,\n): Promise<TillioPullContext> {\n const environment = await resolveEnvironment(credentialsService, scope)\n const integrationState = await readTillioIntegrationState(em, scope)\n const blob = await readOperatorsBlob(credentialsService, scope)\n const operator =\n blob.operators.find((entry) => entry.id === blob.defaultOperatorId) ?? blob.operators[0] ?? null\n\n return {\n readiness: evaluatePullReadiness({\n environment,\n integrationEnabled: integrationState.enabled,\n environmentHealthy: integrationState.healthy,\n operator,\n }),\n environment,\n operator,\n }\n}\n\nfunction buildIngestInput(call: NormalizedPhoneCall, scope: IntegrationScope): Record<string, unknown> {\n return {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n providerKey: TILLIO_PROVIDER_KEY,\n integrationId: TILLIO_INTEGRATION_ID,\n externalCallId: call.externalCallId,\n externalConversationId: call.externalConversationId ?? null,\n direction: call.direction,\n status: call.status,\n participants: call.participants,\n recording: call.recording ?? null,\n startedAt: call.startedAt ?? null,\n answeredAt: call.answeredAt ?? null,\n endedAt: call.endedAt ?? null,\n durationSeconds: call.durationSeconds ?? null,\n providerFacts: call.providerFacts,\n rawPayload: call.rawPayload,\n }\n}\n\n// The initiating user travels in the job payload, so ingest writes carry an actor instead of\n// landing in the audit trail unattributed. The id was resolved from the session that queued\n// the pull, never from request input.\nfunction buildCommandContext(\n container: AwilixContainer,\n scope: IntegrationScope,\n userId: string | null,\n): CommandRuntimeContext {\n return {\n container,\n auth: userId\n ? { sub: userId, tenantId: scope.tenantId, orgId: scope.organizationId, isSuperAdmin: false }\n : null,\n organizationScope: {\n selectedId: scope.organizationId,\n filterIds: [scope.organizationId],\n allowedIds: [scope.organizationId],\n tenantId: scope.tenantId,\n },\n selectedOrganizationId: scope.organizationId,\n organizationIds: [scope.organizationId],\n }\n}\n\nasync function ingestBatch(calls: NormalizedPhoneCall[], deps: IngestDeps): Promise<BatchCounts> {\n const counts: BatchCounts = { created: 0, updated: 0, failed: 0 }\n\n for (const call of calls) {\n try {\n const executed = await deps.commandBus.execute<Record<string, unknown>, IngestPhoneCallResult>(\n PHONE_CALLS_CALL_INGEST_COMMAND_ID,\n { input: buildIngestInput(call, deps.scope), ctx: deps.commandContext },\n )\n if (executed.result.created) counts.created += 1\n else counts.updated += 1\n } catch (err) {\n // One transaction per call, so a single bad record only bumps `failed` and the rest of\n // the batch still lands.\n counts.failed += 1\n logger.warn('ingest failed for a pulled call', { externalCallId: call.externalCallId, err })\n await emitPhoneCallsEvent('phone_calls.call.ingest_failed', {\n providerKey: TILLIO_PROVIDER_KEY,\n externalCallId: call.externalCallId,\n organizationId: deps.scope.organizationId,\n tenantId: deps.scope.tenantId,\n }).catch(() => undefined)\n }\n }\n\n return counts\n}\n\ntype PullFailure = { code: string; message: string }\n\n// The progress bar renders `errorMessage` verbatim and has no path to a translated string, so\n// what goes in has to be ours: a provider message could carry Tillio's own wording, an internal\n// hostname or a token fragment. The raw error stays in the log, the stable code in the summary.\nfunction describeFailure(err: unknown): PullFailure {\n if (err instanceof TillioApiError) {\n return classifyTillioError(err) === 'environment'\n ? { code: 'provider_unreachable', message: 'Tillio could not be reached or rejected the stored credentials.' }\n : { code: 'provider_error', message: 'Tillio rejected the request for calls.' }\n }\n return { code: 'pull_failed', message: 'The Tillio pull could not be completed.' }\n}\n\nfunction describeTruncation(summary: TillioPullSummary, anomaly: PhoneCallBatchAnomaly | null): PullFailure {\n if (anomaly) {\n return {\n code: 'pull_pagination_anomaly',\n message: `Tillio returned an inconsistent page listing, so the sweep stopped early after ${summary.fetched} calls.`,\n }\n }\n return {\n code: 'pull_truncated',\n message: `Stopped after ${summary.batches} batches with more calls left in the range; ${summary.fetched} calls were ingested.`,\n }\n}\n\n// Safe to retry: the ingest command keys on (providerKey, externalCallId) and updates in place,\n// so a job the queue replays after a crash repeats work instead of duplicating rows.\nexport async function runTillioPullJob(params: {\n container: AwilixContainer\n payload: TillioPullJobPayload\n adapter?: PhoneCallProviderAdapter\n}): Promise<TillioPullSummary> {\n const { container, payload } = params\n const adapter = params.adapter ?? tillioAdapter\n const scope: IntegrationScope = {\n organizationId: payload.scope.organizationId,\n tenantId: payload.scope.tenantId,\n }\n\n const progressService = container.resolve('progressService') as ProgressService\n const progressContext: ProgressServiceContext = {\n tenantId: payload.scope.tenantId,\n organizationId: payload.scope.organizationId,\n userId: payload.scope.userId ?? null,\n }\n const summary: TillioPullSummary = {\n fetched: 0,\n created: 0,\n updated: 0,\n failed: 0,\n batches: 0,\n cancelled: false,\n truncated: false,\n resumeCursor: null,\n }\n\n await progressService.startJob(payload.progressJobId, progressContext)\n\n try {\n const credentialsService = container.resolve('integrationCredentialsService') as TillioCredentialsService\n const em = container.resolve('em') as EntityManager\n\n // Not trusted from enqueue time: credentials can rotate or the operator can be detached\n // while the job waits in the queue.\n const { readiness, environment, operator } = await resolvePullContext(credentialsService, em, scope)\n if (readiness.blocker || !environment || !operator) {\n const blocker = readiness.blocker ?? 'environment_not_ready'\n await progressService.failJob(\n payload.progressJobId,\n {\n errorMessage: PULL_BLOCKER_MESSAGES[blocker],\n resultSummary: { ...summary, code: blocker },\n },\n progressContext,\n )\n return summary\n }\n\n const deps: IngestDeps = {\n commandBus: container.resolve('commandBus') as CommandBus,\n commandContext: buildCommandContext(container, scope, payload.scope.userId ?? null),\n scope,\n }\n const credentials = {\n apiUrl: environment.apiUrl,\n apiKey: environment.apiKey,\n tenantSystemId: environment.tenantSystemId,\n timeZone: environment.timeZone,\n operator: {\n id: operator.id,\n plugin: operator.plugin,\n token: operator.token,\n tenantDomain: operator.tenantDomain,\n },\n }\n const fetchBatch = (cursor: string | null): Promise<NormalizedPhoneCallBatch> =>\n adapter.fetchCalls({\n credentials,\n scope,\n integrationId: TILLIO_INTEGRATION_ID,\n from: zonedDayStart(payload.from, environment.timeZone),\n to: zonedDayEnd(payload.to, environment.timeZone),\n cursor,\n limit: payload.limit ?? null,\n })\n\n let cursor = payload.cursor ?? null\n let anomaly: PhoneCallBatchAnomaly | null = null\n\n while (summary.batches < MAX_BATCHES) {\n const cancelled = await progressService.isCancellationRequested(\n payload.progressJobId,\n payload.scope.tenantId,\n payload.scope.organizationId,\n )\n if (cancelled) {\n summary.cancelled = true\n await progressService.markCancelled(payload.progressJobId, progressContext)\n return summary\n }\n\n const batch = await fetchBatch(cursor)\n const counts = await ingestBatch(batch.calls, deps)\n\n summary.batches += 1\n summary.fetched += batch.calls.length\n summary.created += counts.created\n summary.updated += counts.updated\n summary.failed += counts.failed\n\n // Tillio reports no range total, so the job carries a processed count without a total\n // and the top bar renders it without a percentage.\n await progressService.updateProgress(\n payload.progressJobId,\n { processedCount: summary.fetched },\n progressContext,\n )\n\n if (batch.anomaly) {\n anomaly = batch.anomaly\n // Re-running the page we could not trust is safe, because ingest keys on the\n // provider's call id and updates in place.\n summary.resumeCursor = batch.anomalyCursor ?? cursor\n break\n }\n\n cursor = batch.nextCursor ?? null\n if (!cursor) break\n }\n\n // A cursor surviving the loop means the batch cap ended it, not the provider.\n if (!anomaly && cursor) summary.resumeCursor = cursor\n\n if (anomaly || summary.resumeCursor) {\n summary.truncated = true\n const failure = describeTruncation(summary, anomaly)\n logger.warn('the Tillio pull did not cover the whole range', {\n progressJobId: payload.progressJobId,\n code: failure.code,\n anomaly,\n resumeCursor: summary.resumeCursor,\n })\n // No in-between job state exists, and a sweep that silently skipped calls must not read\n // as a clean one. The counts and the resume point ride along so the failure still says\n // what landed and where to pick it up; the calls already ingested stay committed.\n await progressService.failJob(\n payload.progressJobId,\n {\n errorMessage: failure.message,\n resultSummary: { ...summary, code: failure.code, ...(anomaly ? { anomaly } : {}) },\n },\n progressContext,\n )\n return summary\n }\n\n await progressService.completeJob(\n payload.progressJobId,\n { resultSummary: { ...summary } },\n progressContext,\n )\n return summary\n } catch (err) {\n const failure = describeFailure(err)\n logger.error('the Tillio pull failed', { progressJobId: payload.progressJobId, code: failure.code, err })\n try {\n await progressService.failJob(\n payload.progressJobId,\n {\n errorMessage: failure.message,\n errorStack: err instanceof Error ? err.stack?.slice(0, 10000) : undefined,\n resultSummary: { ...summary, code: failure.code },\n },\n progressContext,\n )\n } catch (failErr) {\n logger.error('failed to mark the pull progress job as failed', {\n progressJobId: payload.progressJobId,\n err: failErr,\n })\n }\n throw err\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["cursor"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { computeEnvFingerprint } from "./operators-store.js";
|
|
2
|
+
function evaluateEnvironmentReadiness(input) {
|
|
3
|
+
const ready = Boolean(input.environment) && input.integrationEnabled && input.environmentHealthy;
|
|
4
|
+
if (!input.integrationEnabled && input.environment) return { ready, blocker: "integration_disabled" };
|
|
5
|
+
if (!ready) return { ready, blocker: "environment_not_ready" };
|
|
6
|
+
return { ready, blocker: null };
|
|
7
|
+
}
|
|
8
|
+
function evaluatePullReadiness(input) {
|
|
9
|
+
const environment = evaluateEnvironmentReadiness(input);
|
|
10
|
+
const environmentReady = environment.ready;
|
|
11
|
+
const operatorAttached = Boolean(input.operator);
|
|
12
|
+
const envDrift = Boolean(input.environment) && Boolean(input.operator) && input.operator.envFingerprint !== computeEnvFingerprint(input.environment);
|
|
13
|
+
if (environment.blocker) {
|
|
14
|
+
return { environmentReady, operatorAttached, envDrift, blocker: environment.blocker };
|
|
15
|
+
}
|
|
16
|
+
if (!operatorAttached) return { environmentReady, operatorAttached, envDrift, blocker: "operator_missing" };
|
|
17
|
+
if (envDrift) return { environmentReady, operatorAttached, envDrift, blocker: "environment_drift" };
|
|
18
|
+
return { environmentReady, operatorAttached, envDrift, blocker: null };
|
|
19
|
+
}
|
|
20
|
+
const PULL_BLOCKER_MESSAGES = {
|
|
21
|
+
integration_disabled: "The Tillio integration is disabled. Enable it before pulling calls.",
|
|
22
|
+
environment_not_ready: "Configure the Tillio environment and run the health check first.",
|
|
23
|
+
operator_missing: "Attach a Tillio operator before pulling calls.",
|
|
24
|
+
environment_drift: "The environment changed after this operator was attached. Detach and attach it again before pulling calls."
|
|
25
|
+
};
|
|
26
|
+
function blockerSection(blocker) {
|
|
27
|
+
if (blocker === "integration_disabled" || blocker === "environment_not_ready") return "environment";
|
|
28
|
+
return "operator";
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
PULL_BLOCKER_MESSAGES,
|
|
32
|
+
blockerSection,
|
|
33
|
+
evaluateEnvironmentReadiness,
|
|
34
|
+
evaluatePullReadiness
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=pull-readiness.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/tillio/lib/pull-readiness.ts"],
|
|
4
|
+
"sourcesContent": ["import { computeEnvFingerprint, type TillioOperatorRecord } from './operators-store'\nimport type { TillioResolvedEnvironment } from './operators'\n\n// The two levels the environment alone can be blocked at. Pulling and attaching an operator ask\n// different questions of the operator level, but the same question of this one, so they share it:\n// the `enabled` term was once written out in both places and one copy drifted.\nexport type EnvironmentBlocker = 'integration_disabled' | 'environment_not_ready'\n\nexport type PullBlocker = EnvironmentBlocker | 'operator_missing' | 'environment_drift'\n\nexport type EnvironmentReadiness = {\n ready: boolean\n blocker: EnvironmentBlocker | null\n}\n\nexport type EvaluateEnvironmentReadinessInput = {\n environment: TillioResolvedEnvironment | null\n integrationEnabled: boolean\n environmentHealthy: boolean\n}\n\nexport function evaluateEnvironmentReadiness(input: EvaluateEnvironmentReadinessInput): EnvironmentReadiness {\n const ready = Boolean(input.environment) && input.integrationEnabled && input.environmentHealthy\n // A switched-off integration is reported ahead of health because re-running a health check would\n // not change anything while it is off - but only once there is something to switch on. A missing\n // integration row also reads as disabled, so reporting that first on a fresh install would tell\n // the user to enable an integration they have not configured yet, instead of to configure it.\n if (!input.integrationEnabled && input.environment) return { ready, blocker: 'integration_disabled' }\n if (!ready) return { ready, blocker: 'environment_not_ready' }\n return { ready, blocker: null }\n}\n\nexport type PullReadiness = {\n environmentReady: boolean\n operatorAttached: boolean\n envDrift: boolean\n blocker: PullBlocker | null\n}\n\nexport type EvaluatePullReadinessInput = {\n environment: TillioResolvedEnvironment | null\n integrationEnabled: boolean\n environmentHealthy: boolean\n operator: TillioOperatorRecord | null\n}\n\nexport function evaluatePullReadiness(input: EvaluatePullReadinessInput): PullReadiness {\n const environment = evaluateEnvironmentReadiness(input)\n const environmentReady = environment.ready\n const operatorAttached = Boolean(input.operator)\n const envDrift =\n Boolean(input.environment) &&\n Boolean(input.operator) &&\n input.operator!.envFingerprint !== computeEnvFingerprint(input.environment!)\n\n // Ordered so the reported blocker is the one the user can act on first: an unhealthy environment\n // makes operator state meaningless, and drift only matters once both levels exist. Each code\n // routes the UI to the settings section that fixes it.\n if (environment.blocker) {\n return { environmentReady, operatorAttached, envDrift, blocker: environment.blocker }\n }\n if (!operatorAttached) return { environmentReady, operatorAttached, envDrift, blocker: 'operator_missing' }\n if (envDrift) return { environmentReady, operatorAttached, envDrift, blocker: 'environment_drift' }\n return { environmentReady, operatorAttached, envDrift, blocker: null }\n}\n\nexport const PULL_BLOCKER_MESSAGES: Record<PullBlocker, string> = {\n integration_disabled: 'The Tillio integration is disabled. Enable it before pulling calls.',\n environment_not_ready: 'Configure the Tillio environment and run the health check first.',\n operator_missing: 'Attach a Tillio operator before pulling calls.',\n environment_drift: 'The environment changed after this operator was attached. Detach and attach it again before pulling calls.',\n}\n\nexport function blockerSection(blocker: PullBlocker): 'environment' | 'operator' {\n if (blocker === 'integration_disabled' || blocker === 'environment_not_ready') return 'environment'\n return 'operator'\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,6BAAwD;AAqB1D,SAAS,6BAA6B,OAAgE;AAC3G,QAAM,QAAQ,QAAQ,MAAM,WAAW,KAAK,MAAM,sBAAsB,MAAM;AAK9E,MAAI,CAAC,MAAM,sBAAsB,MAAM,YAAa,QAAO,EAAE,OAAO,SAAS,uBAAuB;AACpG,MAAI,CAAC,MAAO,QAAO,EAAE,OAAO,SAAS,wBAAwB;AAC7D,SAAO,EAAE,OAAO,SAAS,KAAK;AAChC;AAgBO,SAAS,sBAAsB,OAAkD;AACtF,QAAM,cAAc,6BAA6B,KAAK;AACtD,QAAM,mBAAmB,YAAY;AACrC,QAAM,mBAAmB,QAAQ,MAAM,QAAQ;AAC/C,QAAM,WACJ,QAAQ,MAAM,WAAW,KACzB,QAAQ,MAAM,QAAQ,KACtB,MAAM,SAAU,mBAAmB,sBAAsB,MAAM,WAAY;AAK7E,MAAI,YAAY,SAAS;AACvB,WAAO,EAAE,kBAAkB,kBAAkB,UAAU,SAAS,YAAY,QAAQ;AAAA,EACtF;AACA,MAAI,CAAC,iBAAkB,QAAO,EAAE,kBAAkB,kBAAkB,UAAU,SAAS,mBAAmB;AAC1G,MAAI,SAAU,QAAO,EAAE,kBAAkB,kBAAkB,UAAU,SAAS,oBAAoB;AAClG,SAAO,EAAE,kBAAkB,kBAAkB,UAAU,SAAS,KAAK;AACvE;AAEO,MAAM,wBAAqD;AAAA,EAChE,sBAAsB;AAAA,EACtB,uBAAuB;AAAA,EACvB,kBAAkB;AAAA,EAClB,mBAAmB;AACrB;AAEO,SAAS,eAAe,SAAkD;AAC/E,MAAI,YAAY,0BAA0B,YAAY,wBAAyB,QAAO;AACtF,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createModuleQueue } from "@open-mercato/queue";
|
|
2
|
+
import { parseNumberWithDefault } from "@open-mercato/shared/lib/number";
|
|
3
|
+
const queues = /* @__PURE__ */ new Map();
|
|
4
|
+
const TILLIO_PULL_QUEUE = "tillio-pull";
|
|
5
|
+
function resolveTillioQueueConcurrency() {
|
|
6
|
+
return Math.min(20, parseNumberWithDefault(process.env.TILLIO_QUEUE_CONCURRENCY, 1, { min: 1, integer: true }));
|
|
7
|
+
}
|
|
8
|
+
function getTillioQueue(queueName) {
|
|
9
|
+
const existing = queues.get(queueName);
|
|
10
|
+
if (existing) return existing;
|
|
11
|
+
const created = createModuleQueue(queueName, {
|
|
12
|
+
concurrency: resolveTillioQueueConcurrency()
|
|
13
|
+
});
|
|
14
|
+
queues.set(queueName, created);
|
|
15
|
+
return created;
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
TILLIO_PULL_QUEUE,
|
|
19
|
+
getTillioQueue,
|
|
20
|
+
resolveTillioQueueConcurrency
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=queue.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/tillio/lib/queue.ts"],
|
|
4
|
+
"sourcesContent": ["import { createModuleQueue, type Queue } from '@open-mercato/queue'\nimport { parseNumberWithDefault } from '@open-mercato/shared/lib/number'\n\n/**\n * Queue helper for the Tillio provider. Mirrors `getSyncQueue` (data_sync) so the\n * route and the worker share one queue instance per name.\n *\n * Concurrency stays at 1 by default: a pull sweeps Tillio page by page with the\n * operator's token, and running several sweeps at once only buys provider\n * throttling. `TILLIO_QUEUE_CONCURRENCY` raises it for deployments that pull for\n * many organizations at once, capped at the platform ceiling of 20.\n */\nconst queues = new Map<string, Queue<Record<string, unknown>>>()\n\nexport const TILLIO_PULL_QUEUE = 'tillio-pull'\n\n// Worker parallelism comes from the worker manifest, not from the producer-side queue\n// instance this module builds, so both read the same value or the variable only pretends\n// to do something.\nexport function resolveTillioQueueConcurrency(): number {\n return Math.min(20, parseNumberWithDefault(process.env.TILLIO_QUEUE_CONCURRENCY, 1, { min: 1, integer: true }))\n}\n\nexport function getTillioQueue(queueName: string): Queue<Record<string, unknown>> {\n const existing = queues.get(queueName)\n if (existing) return existing\n\n const created = createModuleQueue<Record<string, unknown>>(queueName, {\n concurrency: resolveTillioQueueConcurrency(),\n })\n queues.set(queueName, created)\n return created\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,yBAAqC;AAC9C,SAAS,8BAA8B;AAWvC,MAAM,SAAS,oBAAI,IAA4C;AAExD,MAAM,oBAAoB;AAK1B,SAAS,gCAAwC;AACtD,SAAO,KAAK,IAAI,IAAI,uBAAuB,QAAQ,IAAI,0BAA0B,GAAG,EAAE,KAAK,GAAG,SAAS,KAAK,CAAC,CAAC;AAChH;AAEO,SAAS,eAAe,WAAmD;AAChF,QAAM,WAAW,OAAO,IAAI,SAAS;AACrC,MAAI,SAAU,QAAO;AAErB,QAAM,UAAU,kBAA2C,WAAW;AAAA,IACpE,aAAa,8BAA8B;AAAA,EAC7C,CAAC;AACD,SAAO,IAAI,WAAW,OAAO;AAC7B,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const TILLIO_TIMEZONE = "Europe/Warsaw";
|
|
2
|
+
function isValidTimeZone(value) {
|
|
3
|
+
try {
|
|
4
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value });
|
|
5
|
+
return true;
|
|
6
|
+
} catch {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
11
|
+
function readWallClock(date, timeZone) {
|
|
12
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
13
|
+
timeZone,
|
|
14
|
+
hourCycle: "h23",
|
|
15
|
+
year: "numeric",
|
|
16
|
+
month: "2-digit",
|
|
17
|
+
day: "2-digit",
|
|
18
|
+
hour: "2-digit",
|
|
19
|
+
minute: "2-digit",
|
|
20
|
+
second: "2-digit"
|
|
21
|
+
}).formatToParts(date);
|
|
22
|
+
const read = (type) => Number(parts.find((part) => part.type === type)?.value);
|
|
23
|
+
return {
|
|
24
|
+
year: read("year"),
|
|
25
|
+
month: read("month"),
|
|
26
|
+
day: read("day"),
|
|
27
|
+
hour: read("hour"),
|
|
28
|
+
minute: read("minute"),
|
|
29
|
+
second: read("second")
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function zoneOffsetMs(date, timeZone) {
|
|
33
|
+
const wall = readWallClock(date, timeZone);
|
|
34
|
+
const asUtc = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second);
|
|
35
|
+
return asUtc - (date.getTime() - date.getMilliseconds());
|
|
36
|
+
}
|
|
37
|
+
function zonedWallClockToUtc(day, hour, minute, second, timeZone) {
|
|
38
|
+
if (!DAY_PATTERN.test(day)) {
|
|
39
|
+
throw new Error(`[internal] Expected a YYYY-MM-DD day, received "${day}".`);
|
|
40
|
+
}
|
|
41
|
+
const [year, month, dayOfMonth] = day.split("-").map(Number);
|
|
42
|
+
const naive = Date.UTC(year, month - 1, dayOfMonth, hour, minute, second);
|
|
43
|
+
const firstPass = naive - zoneOffsetMs(new Date(naive), timeZone);
|
|
44
|
+
return new Date(naive - zoneOffsetMs(new Date(firstPass), timeZone));
|
|
45
|
+
}
|
|
46
|
+
function zonedDayStart(day, timeZone = TILLIO_TIMEZONE) {
|
|
47
|
+
return zonedWallClockToUtc(day, 0, 0, 0, timeZone);
|
|
48
|
+
}
|
|
49
|
+
function zonedDayEnd(day, timeZone = TILLIO_TIMEZONE) {
|
|
50
|
+
return zonedWallClockToUtc(day, 23, 59, 0, timeZone);
|
|
51
|
+
}
|
|
52
|
+
function formatTillioTimestamp(date, timeZone = TILLIO_TIMEZONE) {
|
|
53
|
+
const wall = readWallClock(date, timeZone);
|
|
54
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
55
|
+
return `${wall.year}-${pad(wall.month)}-${pad(wall.day)} ${pad(wall.hour)}:${pad(wall.minute)}`;
|
|
56
|
+
}
|
|
57
|
+
const TZ_SUFFIX_PATTERN = /(?:Z|[+-]\d{2}:?\d{2})$/i;
|
|
58
|
+
const WALL_CLOCK_PATTERN = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
59
|
+
function parseTillioTimestamp(value, timeZone = TILLIO_TIMEZONE) {
|
|
60
|
+
if (!value) return null;
|
|
61
|
+
const trimmed = value.trim();
|
|
62
|
+
if (!trimmed) return null;
|
|
63
|
+
if (TZ_SUFFIX_PATTERN.test(trimmed)) {
|
|
64
|
+
const parsed2 = new Date(trimmed);
|
|
65
|
+
return Number.isNaN(parsed2.getTime()) ? null : parsed2;
|
|
66
|
+
}
|
|
67
|
+
const match = WALL_CLOCK_PATTERN.exec(trimmed);
|
|
68
|
+
if (match) {
|
|
69
|
+
const [, day, hour, minute, second] = match;
|
|
70
|
+
const [year, month, dayOfMonth] = day.split("-").map(Number);
|
|
71
|
+
if (year < 1971 || month < 1 || month > 12 || dayOfMonth < 1 || dayOfMonth > 31) return null;
|
|
72
|
+
return zonedWallClockToUtc(day, Number(hour), Number(minute), second ? Number(second) : 0, timeZone);
|
|
73
|
+
}
|
|
74
|
+
const parsed = new Date(trimmed);
|
|
75
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
TILLIO_TIMEZONE,
|
|
79
|
+
formatTillioTimestamp,
|
|
80
|
+
isValidTimeZone,
|
|
81
|
+
parseTillioTimestamp,
|
|
82
|
+
zonedDayEnd,
|
|
83
|
+
zonedDayStart
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=tz.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/tillio/lib/tz.ts"],
|
|
4
|
+
"sourcesContent": ["// The zone Tillio reports its wall-clock timestamps in. Overridable per environment, because an\n// instance serving another market reports that market's local time.\nexport const TILLIO_TIMEZONE = 'Europe/Warsaw'\n\nexport function isValidTimeZone(value: string): boolean {\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: value })\n return true\n } catch {\n return false\n }\n}\n\nconst DAY_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\n\ntype WallClock = {\n year: number\n month: number\n day: number\n hour: number\n minute: number\n second: number\n}\n\nfunction readWallClock(date: Date, timeZone: string): WallClock {\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone,\n hourCycle: 'h23',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n }).formatToParts(date)\n const read = (type: Intl.DateTimeFormatPartTypes): number =>\n Number(parts.find((part) => part.type === type)?.value)\n return {\n year: read('year'),\n month: read('month'),\n day: read('day'),\n hour: read('hour'),\n minute: read('minute'),\n second: read('second'),\n }\n}\n\nfunction zoneOffsetMs(date: Date, timeZone: string): number {\n const wall = readWallClock(date, timeZone)\n const asUtc = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second)\n return asUtc - (date.getTime() - date.getMilliseconds())\n}\n\n// Two passes: the first offset is read at the naive instant, which lands in the\n// wrong side of a DST transition for wall-clock times near the switch. Reading\n// the offset again at the corrected instant settles it.\nfunction zonedWallClockToUtc(\n day: string,\n hour: number,\n minute: number,\n second: number,\n timeZone: string,\n): Date {\n if (!DAY_PATTERN.test(day)) {\n throw new Error(`[internal] Expected a YYYY-MM-DD day, received \"${day}\".`)\n }\n const [year, month, dayOfMonth] = day.split('-').map(Number)\n const naive = Date.UTC(year, month - 1, dayOfMonth, hour, minute, second)\n const firstPass = naive - zoneOffsetMs(new Date(naive), timeZone)\n return new Date(naive - zoneOffsetMs(new Date(firstPass), timeZone))\n}\n\nexport function zonedDayStart(day: string, timeZone: string = TILLIO_TIMEZONE): Date {\n return zonedWallClockToUtc(day, 0, 0, 0, timeZone)\n}\n\n// 23:59, not 23:59:59: Tillio's date filter is `YYYY-MM-DD HH:mm` with no seconds field (see\n// `formatTillioTimestamp`), so a finer bound cannot survive the trip out. Their own API\n// samples use 23:59 as the end of a day for the same reason.\nexport function zonedDayEnd(day: string, timeZone: string = TILLIO_TIMEZONE): Date {\n return zonedWallClockToUtc(day, 23, 59, 0, timeZone)\n}\n\nexport function formatTillioTimestamp(date: Date, timeZone: string = TILLIO_TIMEZONE): string {\n const wall = readWallClock(date, timeZone)\n const pad = (value: number): string => String(value).padStart(2, '0')\n return `${wall.year}-${pad(wall.month)}-${pad(wall.day)} ${pad(wall.hour)}:${pad(wall.minute)}`\n}\n\nconst TZ_SUFFIX_PATTERN = /(?:Z|[+-]\\d{2}:?\\d{2})$/i\nconst WALL_CLOCK_PATTERN = /^(\\d{4}-\\d{2}-\\d{2})[ T](\\d{2}):(\\d{2})(?::(\\d{2}))?$/\n\n// Tillio returns `date` in two shapes depending on the operator:\n// - Ringostat: ISO with an explicit offset (\"2026-04-11T12:47:28+0200\") -> new Date is unambiguous.\n// - Play: a Europe/Warsaw wall-clock time with no zone (\"2026-05-09 08:49:39\"). `new Date` would\n// read it in the process-local zone, which is wrong on any server that is not Warsaw, so we\n// resolve it explicitly through the Tillio timezone instead.\nexport function parseTillioTimestamp(\n value: string | null | undefined,\n timeZone: string = TILLIO_TIMEZONE,\n): Date | null {\n if (!value) return null\n const trimmed = value.trim()\n if (!trimmed) return null\n if (TZ_SUFFIX_PATTERN.test(trimmed)) {\n const parsed = new Date(trimmed)\n return Number.isNaN(parsed.getTime()) ? null : parsed\n }\n const match = WALL_CLOCK_PATTERN.exec(trimmed)\n if (match) {\n const [, day, hour, minute, second] = match\n const [year, month, dayOfMonth] = day.split('-').map(Number)\n // Tillio can emit a zeroed/blank stamp (\"0000-00-00 00:00:00\") for a missing date; it slips\n // past the regex but is not a real calendar day, so treat it as \"no timestamp\" (matching what\n // `new Date` used to yield here: Invalid Date -> null).\n if (year < 1971 || month < 1 || month > 12 || dayOfMonth < 1 || dayOfMonth > 31) return null\n return zonedWallClockToUtc(day, Number(hour), Number(minute), second ? Number(second) : 0, timeZone)\n }\n const parsed = new Date(trimmed)\n return Number.isNaN(parsed.getTime()) ? null : parsed\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["parsed"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { assertStaticallySafeOutboundUrl } from "@open-mercato/shared/lib/url-safety";
|
|
2
|
+
const TILLIO_URL_SUBJECT = "Tillio API URL";
|
|
3
|
+
function assertPublicTillioApiUrl(rawUrl) {
|
|
4
|
+
assertTillioTls(rawUrl);
|
|
5
|
+
assertStaticallySafeOutboundUrl(rawUrl, {
|
|
6
|
+
subject: TILLIO_URL_SUBJECT,
|
|
7
|
+
errorFactory: (_reason, message) => new Error(`${message}.`)
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function assertTillioTls(rawUrl) {
|
|
11
|
+
let protocol;
|
|
12
|
+
try {
|
|
13
|
+
protocol = new URL(rawUrl).protocol;
|
|
14
|
+
} catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (protocol !== "http:") return;
|
|
18
|
+
throw new Error(`${TILLIO_URL_SUBJECT} must use https, so the API key is not sent in cleartext.`);
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
TILLIO_URL_SUBJECT,
|
|
22
|
+
assertPublicTillioApiUrl
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=url-guard.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/tillio/lib/url-guard.ts"],
|
|
4
|
+
"sourcesContent": ["// SSRF guard for the user-supplied Tillio API URL. `apiUrl` comes from integration\n// credentials, so before createTillioClient ever fetches it we reject non-http(s) schemes\n// and hosts that are loopback / private / link-local (cloud metadata endpoints, internal\n// services). This is the synchronous, construction-time check; the resolution-time half\n// (DNS answers, rebinding, redirect targets) is `safeOutboundFetch` in `client.ts`.\n\nimport { assertStaticallySafeOutboundUrl } from '@open-mercato/shared/lib/url-safety'\n\nexport const TILLIO_URL_SUBJECT = 'Tillio API URL'\n\nexport function assertPublicTillioApiUrl(rawUrl: string): void {\n // The shared guard decides where a request may go, not whether what it carries stays\n // confidential, so it admits plaintext http. Every Tillio request sends `X-Api-Key` and,\n // once an operator is attached, `X-Token`, so http is refused here before any of them\n // reaches the wire. No development allowance: `apiUrl` always points at Tillio's public\n // host, and tests stub the transport through `fetchImpl` instead of relaxing this.\n assertTillioTls(rawUrl)\n assertStaticallySafeOutboundUrl(rawUrl, {\n subject: TILLIO_URL_SUBJECT,\n errorFactory: (_reason, message) => new Error(`${message}.`),\n })\n}\n\nfunction assertTillioTls(rawUrl: string): void {\n let protocol: string\n try {\n protocol = new URL(rawUrl).protocol\n } catch {\n // Malformed input belongs to the shared guard so one parser owns that message.\n return\n }\n if (protocol !== 'http:') return\n throw new Error(`${TILLIO_URL_SUBJECT} must use https, so the API key is not sent in cleartext.`)\n}\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,uCAAuC;AAEzC,MAAM,qBAAqB;AAE3B,SAAS,yBAAyB,QAAsB;AAM7D,kBAAgB,MAAM;AACtB,kCAAgC,QAAQ;AAAA,IACtC,SAAS;AAAA,IACT,cAAc,CAAC,SAAS,YAAY,IAAI,MAAM,GAAG,OAAO,GAAG;AAAA,EAC7D,CAAC;AACH;AAEA,SAAS,gBAAgB,QAAsB;AAC7C,MAAI;AACJ,MAAI;AACF,eAAW,IAAI,IAAI,MAAM,EAAE;AAAA,EAC7B,QAAQ;AAEN;AAAA,EACF;AACA,MAAI,aAAa,QAAS;AAC1B,QAAM,IAAI,MAAM,GAAG,kBAAkB,2DAA2D;AAClG;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
import { applyTillioEnvPreset } from "./lib/preset.js";
|
|
3
|
+
import { createTillioLock, tillioOperatorLockKey } from "./lib/locking.js";
|
|
4
|
+
const logger = createLogger("tillio").child({ component: "setup" });
|
|
5
|
+
const setup = {
|
|
6
|
+
defaultRoleFeatures: {
|
|
7
|
+
admin: [
|
|
8
|
+
"tillio.*"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
async seedDefaults({ tenantId, organizationId, container }) {
|
|
12
|
+
try {
|
|
13
|
+
const result = await applyTillioEnvPreset({
|
|
14
|
+
credentialsService: container.resolve("integrationCredentialsService"),
|
|
15
|
+
integrationStateService: container.resolve("integrationStateService"),
|
|
16
|
+
integrationHealthService: container.resolve("integrationHealthService"),
|
|
17
|
+
integrationLogService: container.resolve("integrationLogService"),
|
|
18
|
+
scope: { tenantId, organizationId },
|
|
19
|
+
withLock: createTillioLock(
|
|
20
|
+
container.resolve("em"),
|
|
21
|
+
tillioOperatorLockKey({ tenantId, organizationId })
|
|
22
|
+
)
|
|
23
|
+
});
|
|
24
|
+
if (result.status === "skipped") {
|
|
25
|
+
logger.debug("env preset not applied", { reason: result.reason });
|
|
26
|
+
} else if (result.status === "blocked") {
|
|
27
|
+
logger.warn("env preset refused", { reason: result.reason });
|
|
28
|
+
} else {
|
|
29
|
+
logger.info("env preset applied", {
|
|
30
|
+
credentialsAction: result.credentialsAction,
|
|
31
|
+
health: result.health,
|
|
32
|
+
operator: result.operator
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
logger.warn("failed to apply the env preset during tenant setup", { err });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var setup_default = setup;
|
|
41
|
+
export {
|
|
42
|
+
setup_default as default,
|
|
43
|
+
setup
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/tillio/setup.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { applyTillioEnvPreset } from './lib/preset'\nimport { createTillioLock, tillioOperatorLockKey } from './lib/locking'\nimport type { TillioCredentialsService } from './lib/operators-store'\n\nconst logger = createLogger('tillio').child({ component: 'setup' })\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: [\n 'tillio.*',\n ],\n },\n\n async seedDefaults({ tenantId, organizationId, container }) {\n try {\n const result = await applyTillioEnvPreset({\n credentialsService: container.resolve('integrationCredentialsService') as TillioCredentialsService,\n integrationStateService: container.resolve('integrationStateService'),\n integrationHealthService: container.resolve('integrationHealthService'),\n integrationLogService: container.resolve('integrationLogService'),\n scope: { tenantId, organizationId },\n withLock: createTillioLock(\n container.resolve('em') as EntityManager,\n tillioOperatorLockKey({ tenantId, organizationId }),\n ),\n })\n if (result.status === 'skipped') {\n logger.debug('env preset not applied', { reason: result.reason })\n } else if (result.status === 'blocked') {\n logger.warn('env preset refused', { reason: result.reason })\n } else {\n logger.info('env preset applied', {\n credentialsAction: result.credentialsAction,\n health: result.health,\n operator: result.operator,\n })\n }\n } catch (err) {\n // A provider preset is never worth failing tenant bootstrap over; the operator can rerun\n // `mercato tillio configure-from-env` once the deployment is fixed.\n logger.warn('failed to apply the env preset during tenant setup', { err })\n }\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,oBAAoB;AAE7B,SAAS,4BAA4B;AACrC,SAAS,kBAAkB,6BAA6B;AAGxD,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,QAAQ,CAAC;AAE3D,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO;AAAA,MACL;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,EAAE,UAAU,gBAAgB,UAAU,GAAG;AAC1D,QAAI;AACF,YAAM,SAAS,MAAM,qBAAqB;AAAA,QACxC,oBAAoB,UAAU,QAAQ,+BAA+B;AAAA,QACrE,yBAAyB,UAAU,QAAQ,yBAAyB;AAAA,QACpE,0BAA0B,UAAU,QAAQ,0BAA0B;AAAA,QACtE,uBAAuB,UAAU,QAAQ,uBAAuB;AAAA,QAChE,OAAO,EAAE,UAAU,eAAe;AAAA,QAClC,UAAU;AAAA,UACR,UAAU,QAAQ,IAAI;AAAA,UACtB,sBAAsB,EAAE,UAAU,eAAe,CAAC;AAAA,QACpD;AAAA,MACF,CAAC;AACD,UAAI,OAAO,WAAW,WAAW;AAC/B,eAAO,MAAM,0BAA0B,EAAE,QAAQ,OAAO,OAAO,CAAC;AAAA,MAClE,WAAW,OAAO,WAAW,WAAW;AACtC,eAAO,KAAK,sBAAsB,EAAE,QAAQ,OAAO,OAAO,CAAC;AAAA,MAC7D,OAAO;AACL,eAAO,KAAK,sBAAsB;AAAA,UAChC,mBAAmB,OAAO;AAAA,UAC1B,QAAQ,OAAO;AAAA,UACf,UAAU,OAAO;AAAA,QACnB,CAAC;AAAA,MACH;AAAA,IACF,SAAS,KAAK;AAGZ,aAAO,KAAK,sDAAsD,EAAE,IAAI,CAAC;AAAA,IAC3E;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|