@llm-dev-ops/agentics-cli 1.4.87 → 1.4.90
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/dist/__tests__/api_gateway.test.d.ts +1 -0
- package/dist/__tests__/api_gateway.test.js +50 -0
- package/dist/__tests__/domain_enterprise_solution.test.d.ts +1 -0
- package/dist/__tests__/domain_enterprise_solution.test.js +50 -0
- package/dist/__tests__/health.test.d.ts +1 -0
- package/dist/__tests__/health.test.js +19 -0
- package/dist/__tests__/monitoring_alerts.test.d.ts +1 -0
- package/dist/__tests__/monitoring_alerts.test.js +50 -0
- package/dist/__tests__/ongoing_regular_stakeholder.test.d.ts +1 -0
- package/dist/__tests__/ongoing_regular_stakeholder.test.js +50 -0
- package/dist/__tests__/re_evaluate_risk.test.d.ts +1 -0
- package/dist/__tests__/re_evaluate_risk.test.js +50 -0
- package/dist/__tests__/recommendation.test.d.ts +1 -0
- package/dist/__tests__/recommendation.test.js +50 -0
- package/dist/__tests__/risk_mitigation_plan.test.d.ts +1 -0
- package/dist/__tests__/risk_mitigation_plan.test.js +50 -0
- package/dist/__tests__/scoped_pilot_reduce.test.d.ts +1 -0
- package/dist/__tests__/scoped_pilot_reduce.test.js +50 -0
- package/dist/__tests__/target_enterprise_system.test.d.ts +1 -0
- package/dist/__tests__/target_enterprise_system.test.js +50 -0
- package/dist/__tests__/underwriting.test.d.ts +1 -0
- package/dist/__tests__/underwriting.test.js +50 -0
- package/dist/api-gateway/infra/api-gateway-adapter.d.ts +27 -0
- package/dist/api-gateway/infra/api-gateway-adapter.js +54 -0
- package/dist/api-gateway/ports/api-gateway.d.ts +26 -0
- package/dist/api-gateway/ports/api-gateway.js +2 -0
- package/dist/config/endpoints.js +30 -30
- package/dist/config/endpoints.js.map +1 -1
- package/dist/contracts/contract-validation.d.ts +11 -0
- package/dist/contracts/contract-validation.js +21 -0
- package/dist/domain-enterprise-solution/infra/api-gateway-seam-adapter.d.ts +21 -0
- package/dist/domain-enterprise-solution/infra/api-gateway-seam-adapter.js +42 -0
- package/dist/domain-enterprise-solution/infra/domain-enterprise-solution-adapter.d.ts +25 -0
- package/dist/domain-enterprise-solution/infra/domain-enterprise-solution-adapter.js +47 -0
- package/dist/domain-enterprise-solution/ports/api-gateway-seam.d.ts +20 -0
- package/dist/domain-enterprise-solution/ports/api-gateway-seam.js +2 -0
- package/dist/domain-enterprise-solution/ports/domain-enterprise-solution.d.ts +24 -0
- package/dist/domain-enterprise-solution/ports/domain-enterprise-solution.js +2 -0
- package/dist/enterprise/index.d.ts +15 -0
- package/dist/enterprise/index.js +16 -0
- package/dist/erp-client/client.d.ts +42 -0
- package/dist/erp-client/client.js +235 -0
- package/dist/erp-client/mapper.d.ts +9 -0
- package/dist/erp-client/mapper.js +116 -0
- package/dist/erp-client/retry.d.ts +17 -0
- package/dist/erp-client/retry.js +74 -0
- package/dist/erp-client/types.d.ts +155 -0
- package/dist/erp-client/types.js +2 -0
- package/dist/infra/clients.d.ts +27 -0
- package/dist/infra/clients.js +16 -0
- package/dist/infra/connection-pool.d.ts +16 -0
- package/dist/infra/connection-pool.js +13 -0
- package/dist/infra/iam-helper.d.ts +1 -0
- package/dist/infra/iam-helper.js +138 -0
- package/dist/infra/telemetry.d.ts +26 -0
- package/dist/infra/telemetry.js +39 -0
- package/dist/monitoring-alerts/infra/monitoring-alerts-adapter.d.ts +25 -0
- package/dist/monitoring-alerts/infra/monitoring-alerts-adapter.js +47 -0
- package/dist/monitoring-alerts/ports/monitoring-alerts.d.ts +24 -0
- package/dist/monitoring-alerts/ports/monitoring-alerts.js +2 -0
- package/dist/ongoing-regular-stakeholder/infra/ongoing-regular-stakeholder-adapter.d.ts +25 -0
- package/dist/ongoing-regular-stakeholder/infra/ongoing-regular-stakeholder-adapter.js +47 -0
- package/dist/ongoing-regular-stakeholder/ports/ongoing-regular-stakeholder.d.ts +24 -0
- package/dist/ongoing-regular-stakeholder/ports/ongoing-regular-stakeholder.js +2 -0
- package/dist/pipeline/phase2/phase2-coordinator.d.ts.map +1 -1
- package/dist/pipeline/phase2/phase2-coordinator.js +7 -3
- package/dist/pipeline/phase2/phase2-coordinator.js.map +1 -1
- package/dist/pipeline/ruflo-phase-executor.js +1 -1
- package/dist/pipeline/ruflo-phase-executor.js.map +1 -1
- package/dist/re-evaluate-risk/infra/re-evaluate-risk-adapter.d.ts +25 -0
- package/dist/re-evaluate-risk/infra/re-evaluate-risk-adapter.js +47 -0
- package/dist/re-evaluate-risk/ports/re-evaluate-risk.d.ts +24 -0
- package/dist/re-evaluate-risk/ports/re-evaluate-risk.js +2 -0
- package/dist/recommendation/infra/recommendation-adapter.d.ts +25 -0
- package/dist/recommendation/infra/recommendation-adapter.js +47 -0
- package/dist/recommendation/ports/recommendation.d.ts +24 -0
- package/dist/recommendation/ports/recommendation.js +2 -0
- package/dist/risk-mitigation-plan/infra/risk-mitigation-plan-adapter.d.ts +25 -0
- package/dist/risk-mitigation-plan/infra/risk-mitigation-plan-adapter.js +47 -0
- package/dist/risk-mitigation-plan/ports/risk-mitigation-plan.d.ts +24 -0
- package/dist/risk-mitigation-plan/ports/risk-mitigation-plan.js +2 -0
- package/dist/scoped-pilot-reduce/infra/scoped-pilot-reduce-adapter.d.ts +25 -0
- package/dist/scoped-pilot-reduce/infra/scoped-pilot-reduce-adapter.js +47 -0
- package/dist/scoped-pilot-reduce/ports/scoped-pilot-reduce.d.ts +24 -0
- package/dist/scoped-pilot-reduce/ports/scoped-pilot-reduce.js +2 -0
- package/dist/server/dependencies.d.ts +178 -0
- package/dist/server/dependencies.js +321 -0
- package/dist/server/health.d.ts +2 -0
- package/dist/server/health.js +9 -0
- package/dist/server/main.d.ts +1 -0
- package/dist/server/main.js +21 -0
- package/dist/server/middleware.d.ts +4 -0
- package/dist/server/middleware.js +106 -0
- package/dist/server/routes.d.ts +5 -0
- package/dist/server/routes.js +1100 -0
- package/dist/server/schemas.d.ts +217 -0
- package/dist/server/schemas.js +185 -0
- package/dist/target-enterprise-system/infra/target-enterprise-system-adapter.d.ts +25 -0
- package/dist/target-enterprise-system/infra/target-enterprise-system-adapter.js +47 -0
- package/dist/target-enterprise-system/ports/target-enterprise-system.d.ts +24 -0
- package/dist/target-enterprise-system/ports/target-enterprise-system.js +2 -0
- package/dist/underwriting/infra/underwriting-adapter.d.ts +25 -0
- package/dist/underwriting/infra/underwriting-adapter.js +47 -0
- package/dist/underwriting/ports/underwriting.d.ts +24 -0
- package/dist/underwriting/ports/underwriting.js +2 -0
- package/docs/ecosystem.graph.json +295 -214
- package/package.json +1 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Generated by Phase 4 pipeline — do not edit manually
|
|
2
|
+
import { InforMapper } from './mapper.js';
|
|
3
|
+
import { withRetry, CircuitBreaker } from './retry.js';
|
|
4
|
+
const GCP_METADATA_TOKEN_URL = 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity';
|
|
5
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// InforClient — Anti-Corruption Layer
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export class InforClient {
|
|
10
|
+
baseUrl;
|
|
11
|
+
serviceAccountEmail;
|
|
12
|
+
mapper;
|
|
13
|
+
breaker;
|
|
14
|
+
governance;
|
|
15
|
+
audit;
|
|
16
|
+
syncQueue = [];
|
|
17
|
+
recentErrors = [];
|
|
18
|
+
lastSuccessfulSync = null;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
21
|
+
this.serviceAccountEmail = config.serviceAccountEmail;
|
|
22
|
+
this.governance = config.governance;
|
|
23
|
+
this.mapper = new InforMapper(config.governance);
|
|
24
|
+
this.breaker = new CircuitBreaker(config.circuitBreakerThreshold ?? 5, config.circuitBreakerResetMs ?? 60_000);
|
|
25
|
+
this.audit = config.audit ?? null;
|
|
26
|
+
}
|
|
27
|
+
// --------------------------------------------------------------------------
|
|
28
|
+
// Governance Guard
|
|
29
|
+
// --------------------------------------------------------------------------
|
|
30
|
+
assertWriteAllowed(operation) {
|
|
31
|
+
if (this.governance.read_only) {
|
|
32
|
+
throw new Error(`Governance: write operation '${operation}' blocked — read_only mode is enabled`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// --------------------------------------------------------------------------
|
|
36
|
+
// Domain Operations (ACL boundary)
|
|
37
|
+
// --------------------------------------------------------------------------
|
|
38
|
+
async createTransferOrder(domain, correlationId) {
|
|
39
|
+
this.assertWriteAllowed('createTransferOrder');
|
|
40
|
+
const wire = this.mapper.toTransferOrder(domain);
|
|
41
|
+
return this.executeWithFallback('createTransferOrder', domain.id, correlationId, async () => {
|
|
42
|
+
return this.post('/M3/m3api-rest/v2/execute/MMS850MI/AddTransferOrder', wire, correlationId);
|
|
43
|
+
}, wire);
|
|
44
|
+
}
|
|
45
|
+
async createPurchaseOrder(domain, correlationId) {
|
|
46
|
+
this.assertWriteAllowed('createPurchaseOrder');
|
|
47
|
+
const wire = this.mapper.toPurchaseOrder(domain);
|
|
48
|
+
return this.executeWithFallback('createPurchaseOrder', domain.id, correlationId, async () => {
|
|
49
|
+
return this.post('/M3/m3api-rest/v2/execute/PPS200MI/AddPurchaseOrder', wire, correlationId);
|
|
50
|
+
}, wire);
|
|
51
|
+
}
|
|
52
|
+
async getInventoryBalance(subsidiary, location, item, correlationId) {
|
|
53
|
+
await this.auditBefore('getInventoryBalance', 'inventory', correlationId, { subsidiary, location, item });
|
|
54
|
+
try {
|
|
55
|
+
const response = await this.breaker.execute(() => withRetry(() => this.get(`/M3/m3api-rest/v2/execute/MMS200MI/GetItemBalance?subsidiary=${encodeURIComponent(subsidiary)}&location=${encodeURIComponent(location)}&item=${encodeURIComponent(item)}`, correlationId)));
|
|
56
|
+
this.lastSuccessfulSync = new Date().toISOString();
|
|
57
|
+
await this.auditAfter('getInventoryBalance', 'inventory', correlationId, response);
|
|
58
|
+
if (response.status === 200 && response.data) {
|
|
59
|
+
return this.mapper.fromInventoryBalance(response.data);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
this.recordError('getInventoryBalance', err);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async updateItemReceipt(domain, transferNsId, correlationId) {
|
|
69
|
+
this.assertWriteAllowed('updateItemReceipt');
|
|
70
|
+
const wire = this.mapper.toItemReceipt(domain, transferNsId);
|
|
71
|
+
return this.executeWithFallback('updateItemReceipt', domain.transfer_id, correlationId, async () => {
|
|
72
|
+
return this.post('/M3/m3api-rest/v2/execute/PPS300MI/AddReceipt', wire, correlationId);
|
|
73
|
+
}, wire);
|
|
74
|
+
}
|
|
75
|
+
// --------------------------------------------------------------------------
|
|
76
|
+
// Sync Queue & Status
|
|
77
|
+
// --------------------------------------------------------------------------
|
|
78
|
+
getSyncStatus() {
|
|
79
|
+
return {
|
|
80
|
+
last_successful_sync: this.lastSuccessfulSync,
|
|
81
|
+
pending_queue_depth: this.syncQueue.length,
|
|
82
|
+
circuit_breaker_state: this.breaker.state,
|
|
83
|
+
recent_errors: this.recentErrors.slice(-20),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
getPendingQueue() {
|
|
87
|
+
return this.syncQueue;
|
|
88
|
+
}
|
|
89
|
+
async drainSyncQueue() {
|
|
90
|
+
let processed = 0;
|
|
91
|
+
let failed = 0;
|
|
92
|
+
const remaining = [];
|
|
93
|
+
for (const entry of this.syncQueue) {
|
|
94
|
+
try {
|
|
95
|
+
await this.breaker.execute(() => withRetry(() => this.post(this.operationToPath(entry.operation), entry.payload, entry.id)));
|
|
96
|
+
processed++;
|
|
97
|
+
this.lastSuccessfulSync = new Date().toISOString();
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
failed++;
|
|
101
|
+
remaining.push({
|
|
102
|
+
...entry,
|
|
103
|
+
retry_count: entry.retry_count + 1,
|
|
104
|
+
last_error: err instanceof Error ? err.message : String(err),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
this.syncQueue.length = 0;
|
|
109
|
+
this.syncQueue.push(...remaining);
|
|
110
|
+
return { processed, failed };
|
|
111
|
+
}
|
|
112
|
+
// --------------------------------------------------------------------------
|
|
113
|
+
// Internal: HTTP Transport
|
|
114
|
+
// --------------------------------------------------------------------------
|
|
115
|
+
async post(path, body, correlationId) {
|
|
116
|
+
const token = await this.getIdentityToken();
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
'Authorization': `Bearer ${token}`,
|
|
125
|
+
'X-Correlation-ID': correlationId,
|
|
126
|
+
},
|
|
127
|
+
body: JSON.stringify(body),
|
|
128
|
+
signal: controller.signal,
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
throw new Error(`Infor returned HTTP ${response.status}: ${await response.text()}`);
|
|
132
|
+
}
|
|
133
|
+
return response.json();
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
clearTimeout(timeoutId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async get(path, correlationId) {
|
|
140
|
+
const token = await this.getIdentityToken();
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
145
|
+
method: 'GET',
|
|
146
|
+
headers: {
|
|
147
|
+
'Authorization': `Bearer ${token}`,
|
|
148
|
+
'X-Correlation-ID': correlationId,
|
|
149
|
+
},
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(`Infor returned HTTP ${response.status}: ${await response.text()}`);
|
|
154
|
+
}
|
|
155
|
+
return response.json();
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
clearTimeout(timeoutId);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async getIdentityToken() {
|
|
162
|
+
const url = new URL(GCP_METADATA_TOKEN_URL);
|
|
163
|
+
url.searchParams.set('audience', this.serviceAccountEmail);
|
|
164
|
+
const response = await fetch(url.toString(), {
|
|
165
|
+
headers: { 'Metadata-Flavor': 'Google' },
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(`Failed to obtain GCP identity token: HTTP ${response.status}`);
|
|
169
|
+
}
|
|
170
|
+
return response.text();
|
|
171
|
+
}
|
|
172
|
+
// --------------------------------------------------------------------------
|
|
173
|
+
// Internal: Fallback & Queue
|
|
174
|
+
// --------------------------------------------------------------------------
|
|
175
|
+
async executeWithFallback(operation, entityId, correlationId, fn, payload) {
|
|
176
|
+
await this.auditBefore(operation, entityId, correlationId, payload);
|
|
177
|
+
try {
|
|
178
|
+
const result = await this.breaker.execute(() => withRetry(fn));
|
|
179
|
+
this.lastSuccessfulSync = new Date().toISOString();
|
|
180
|
+
await this.auditAfter(operation, entityId, correlationId, result);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
this.recordError(operation, err);
|
|
185
|
+
// Queue for retry when ERP is unreachable
|
|
186
|
+
this.syncQueue.push({
|
|
187
|
+
id: correlationId,
|
|
188
|
+
operation,
|
|
189
|
+
payload,
|
|
190
|
+
created_at: new Date().toISOString(),
|
|
191
|
+
retry_count: 0,
|
|
192
|
+
last_error: err instanceof Error ? err.message : String(err),
|
|
193
|
+
});
|
|
194
|
+
// Return a queued response so domain services can continue
|
|
195
|
+
return {
|
|
196
|
+
id: correlationId,
|
|
197
|
+
status: 202,
|
|
198
|
+
message: `Queued for sync — Infor temporarily unreachable`,
|
|
199
|
+
data: { queued: true, operation, entity_id: entityId },
|
|
200
|
+
correlation_id: correlationId,
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
recordError(operation, err) {
|
|
206
|
+
this.recentErrors.push({
|
|
207
|
+
timestamp: new Date().toISOString(),
|
|
208
|
+
operation,
|
|
209
|
+
error: err instanceof Error ? err.message : String(err),
|
|
210
|
+
});
|
|
211
|
+
if (this.recentErrors.length > 100)
|
|
212
|
+
this.recentErrors.splice(0, this.recentErrors.length - 100);
|
|
213
|
+
}
|
|
214
|
+
operationToPath(operation) {
|
|
215
|
+
const paths = {
|
|
216
|
+
createTransferOrder: '/M3/m3api-rest/v2/execute/MMS850MI/AddTransferOrder',
|
|
217
|
+
createPurchaseOrder: '/M3/m3api-rest/v2/execute/PPS200MI/AddPurchaseOrder',
|
|
218
|
+
updateItemReceipt: '/M3/m3api-rest/v2/execute/PPS300MI/AddReceipt',
|
|
219
|
+
};
|
|
220
|
+
return paths[operation] ?? '/M3/m3api-rest/v2/execute/ENTITY';
|
|
221
|
+
}
|
|
222
|
+
// --------------------------------------------------------------------------
|
|
223
|
+
// Internal: Audit Logging
|
|
224
|
+
// --------------------------------------------------------------------------
|
|
225
|
+
async auditBefore(operation, entityId, correlationId, payload) {
|
|
226
|
+
if (!this.governance.audit_enabled || !this.audit)
|
|
227
|
+
return;
|
|
228
|
+
await this.audit.log('infor_cloudsuite_call', entityId, `${operation}:before`, correlationId, payload);
|
|
229
|
+
}
|
|
230
|
+
async auditAfter(operation, entityId, correlationId, result) {
|
|
231
|
+
if (!this.governance.audit_enabled || !this.audit)
|
|
232
|
+
return;
|
|
233
|
+
await this.audit.log('infor_cloudsuite_call', entityId, `${operation}:after`, correlationId, result);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DomainTransferOrder, DomainPurchaseOrder, DomainInventoryBalance, DomainItemReceipt, InforTransferOrder, InforPurchaseOrder, InforInventoryBalance, InforItemReceipt, GovernanceConfig } from './types.js';
|
|
2
|
+
export declare class InforMapper {
|
|
3
|
+
private readonly governance;
|
|
4
|
+
constructor(governance: GovernanceConfig);
|
|
5
|
+
toTransferOrder(domain: DomainTransferOrder): InforTransferOrder;
|
|
6
|
+
toPurchaseOrder(domain: DomainPurchaseOrder): InforPurchaseOrder;
|
|
7
|
+
toItemReceipt(domain: DomainItemReceipt, transferNsId: string): InforItemReceipt;
|
|
8
|
+
fromInventoryBalance(ns: InforInventoryBalance): DomainInventoryBalance;
|
|
9
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Generated by Phase 4 pipeline — do not edit manually
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// PII Redaction
|
|
4
|
+
// ============================================================================
|
|
5
|
+
const PII_PATTERNS = [
|
|
6
|
+
/\b\d{3}-\d{2}-\d{4}\b/g, // SSN
|
|
7
|
+
/\b[A-Z]{2}\d{6,9}\b/g, // Passport
|
|
8
|
+
/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, // Credit card
|
|
9
|
+
/patient[_\s]?(?:id|name|mrn)/gi, // Patient identifiers
|
|
10
|
+
];
|
|
11
|
+
function redactPii(value) {
|
|
12
|
+
let result = value;
|
|
13
|
+
for (const pattern of PII_PATTERNS) {
|
|
14
|
+
result = result.replace(pattern, '[REDACTED]');
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
function redactObject(obj, governance) {
|
|
19
|
+
if (!governance.pii_redaction)
|
|
20
|
+
return obj;
|
|
21
|
+
const result = {};
|
|
22
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
23
|
+
if (typeof value === 'string') {
|
|
24
|
+
result[key] = redactPii(value);
|
|
25
|
+
}
|
|
26
|
+
else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
27
|
+
result[key] = redactObject(value, governance);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
result[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// InforMapper — Domain ↔ Wire Type Translation
|
|
37
|
+
// ============================================================================
|
|
38
|
+
export class InforMapper {
|
|
39
|
+
governance;
|
|
40
|
+
constructor(governance) {
|
|
41
|
+
this.governance = governance;
|
|
42
|
+
}
|
|
43
|
+
// --------------------------------------------------------------------------
|
|
44
|
+
// Domain → Infor (outbound)
|
|
45
|
+
// --------------------------------------------------------------------------
|
|
46
|
+
toTransferOrder(domain) {
|
|
47
|
+
const wire = {
|
|
48
|
+
subsidiary: { id: '1' },
|
|
49
|
+
location: { id: domain.from_facility },
|
|
50
|
+
transferLocation: { id: domain.to_facility },
|
|
51
|
+
memo: `Transfer ${domain.sku} — priority: ${domain.priority}`,
|
|
52
|
+
item: {
|
|
53
|
+
items: [{
|
|
54
|
+
item: { id: domain.sku },
|
|
55
|
+
quantity: domain.quantity,
|
|
56
|
+
units: { id: '1' },
|
|
57
|
+
}],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
if (this.governance.pii_redaction) {
|
|
61
|
+
return redactObject(wire, this.governance);
|
|
62
|
+
{
|
|
63
|
+
wp;
|
|
64
|
+
}
|
|
65
|
+
TransferOrder;
|
|
66
|
+
}
|
|
67
|
+
return wire;
|
|
68
|
+
}
|
|
69
|
+
toPurchaseOrder(domain) {
|
|
70
|
+
const wire = {
|
|
71
|
+
entity: { id: domain.vendor_id },
|
|
72
|
+
subsidiary: { id: '1' },
|
|
73
|
+
location: { id: domain.facility_id },
|
|
74
|
+
memo: `PO for ${domain.sku} — qty: ${domain.quantity}`,
|
|
75
|
+
item: {
|
|
76
|
+
items: [{
|
|
77
|
+
item: { id: domain.sku },
|
|
78
|
+
quantity: domain.quantity,
|
|
79
|
+
}],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
if (this.governance.pii_redaction) {
|
|
83
|
+
return redactObject(wire, this.governance);
|
|
84
|
+
{
|
|
85
|
+
wp;
|
|
86
|
+
}
|
|
87
|
+
PurchaseOrder;
|
|
88
|
+
}
|
|
89
|
+
return wire;
|
|
90
|
+
}
|
|
91
|
+
toItemReceipt(domain, transferNsId) {
|
|
92
|
+
return {
|
|
93
|
+
createdFrom: { id: transferNsId },
|
|
94
|
+
subsidiary: { id: '1' },
|
|
95
|
+
item: {
|
|
96
|
+
items: [{
|
|
97
|
+
item: { id: domain.sku },
|
|
98
|
+
quantity: domain.quantity_received,
|
|
99
|
+
location: { id: domain.facility_id },
|
|
100
|
+
}],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// --------------------------------------------------------------------------
|
|
105
|
+
// Infor → Domain (inbound)
|
|
106
|
+
// --------------------------------------------------------------------------
|
|
107
|
+
fromInventoryBalance(ns) {
|
|
108
|
+
return {
|
|
109
|
+
facility_id: ns.location.id,
|
|
110
|
+
sku: ns.item.id,
|
|
111
|
+
quantity: ns.quantityAvailable,
|
|
112
|
+
unit_of_measure: 'EA',
|
|
113
|
+
last_synced_at: ns.lastModifiedDate,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface RetryConfig {
|
|
2
|
+
readonly maxAttempts: number;
|
|
3
|
+
readonly initialDelayMs: number;
|
|
4
|
+
readonly multiplier: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function withRetry<T>(fn: () => Promise<T>, config?: RetryConfig): Promise<T>;
|
|
7
|
+
export declare class CircuitBreaker {
|
|
8
|
+
private readonly threshold;
|
|
9
|
+
private readonly resetTimeoutMs;
|
|
10
|
+
private failures;
|
|
11
|
+
private openedAt;
|
|
12
|
+
constructor(threshold?: number, resetTimeoutMs?: number);
|
|
13
|
+
get state(): 'closed' | 'open' | 'half-open';
|
|
14
|
+
get isOpen(): boolean;
|
|
15
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
16
|
+
reset(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Generated by Phase 4 pipeline — do not edit manually
|
|
2
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
3
|
+
maxAttempts: 3,
|
|
4
|
+
initialDelayMs: 1_000,
|
|
5
|
+
multiplier: 2,
|
|
6
|
+
};
|
|
7
|
+
export async function withRetry(fn, config = DEFAULT_RETRY_CONFIG) {
|
|
8
|
+
let lastError;
|
|
9
|
+
let delayMs = config.initialDelayMs;
|
|
10
|
+
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
|
|
11
|
+
try {
|
|
12
|
+
return await fn();
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
lastError = err;
|
|
16
|
+
if (attempt < config.maxAttempts) {
|
|
17
|
+
await sleep(delayMs);
|
|
18
|
+
delayMs *= config.multiplier;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
throw lastError;
|
|
23
|
+
}
|
|
24
|
+
function sleep(ms) {
|
|
25
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
}
|
|
27
|
+
export class CircuitBreaker {
|
|
28
|
+
threshold;
|
|
29
|
+
resetTimeoutMs;
|
|
30
|
+
failures = 0;
|
|
31
|
+
openedAt = null;
|
|
32
|
+
constructor(threshold = 5, resetTimeoutMs = 60_000) {
|
|
33
|
+
this.threshold = threshold;
|
|
34
|
+
this.resetTimeoutMs = resetTimeoutMs;
|
|
35
|
+
}
|
|
36
|
+
get state() {
|
|
37
|
+
if (this.openedAt === null)
|
|
38
|
+
return 'closed';
|
|
39
|
+
if (Date.now() - this.openedAt >= this.resetTimeoutMs)
|
|
40
|
+
return 'half-open';
|
|
41
|
+
return 'open';
|
|
42
|
+
}
|
|
43
|
+
get isOpen() {
|
|
44
|
+
if (this.openedAt === null)
|
|
45
|
+
return false;
|
|
46
|
+
if (Date.now() - this.openedAt >= this.resetTimeoutMs) {
|
|
47
|
+
this.openedAt = null;
|
|
48
|
+
this.failures = 0;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
async execute(fn) {
|
|
54
|
+
if (this.isOpen) {
|
|
55
|
+
throw new Error('Circuit breaker is open — ERP service unavailable');
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const result = await fn();
|
|
59
|
+
this.failures = 0;
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
this.failures++;
|
|
64
|
+
if (this.failures >= this.threshold) {
|
|
65
|
+
this.openedAt = Date.now();
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
reset() {
|
|
71
|
+
this.failures = 0;
|
|
72
|
+
this.openedAt = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ERP surface types for infor.
|
|
3
|
+
* Domain types and Infor wire format types for the ACL boundary.
|
|
4
|
+
*/
|
|
5
|
+
export type ErpSystemType = 'infor';
|
|
6
|
+
export interface ErpSurfaceRequest {
|
|
7
|
+
readonly erp_type: ErpSystemType;
|
|
8
|
+
readonly operation: string;
|
|
9
|
+
readonly entity_type: string;
|
|
10
|
+
readonly query?: Record<string, unknown>;
|
|
11
|
+
readonly correlation_id: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ErpSurfaceResponse {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly status: number;
|
|
16
|
+
readonly message: string;
|
|
17
|
+
readonly data: unknown;
|
|
18
|
+
readonly correlation_id: string;
|
|
19
|
+
readonly timestamp: string;
|
|
20
|
+
}
|
|
21
|
+
export interface DomainTransferOrder {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly from_facility: string;
|
|
24
|
+
readonly to_facility: string;
|
|
25
|
+
readonly sku: string;
|
|
26
|
+
readonly quantity: number;
|
|
27
|
+
readonly status: string;
|
|
28
|
+
readonly priority: string;
|
|
29
|
+
}
|
|
30
|
+
export interface DomainPurchaseOrder {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly facility_id: string;
|
|
33
|
+
readonly sku: string;
|
|
34
|
+
readonly quantity: number;
|
|
35
|
+
readonly vendor_id: string;
|
|
36
|
+
readonly status: string;
|
|
37
|
+
}
|
|
38
|
+
export interface DomainInventoryBalance {
|
|
39
|
+
readonly facility_id: string;
|
|
40
|
+
readonly sku: string;
|
|
41
|
+
readonly quantity: number;
|
|
42
|
+
readonly unit_of_measure: string;
|
|
43
|
+
readonly last_synced_at: string;
|
|
44
|
+
}
|
|
45
|
+
export interface DomainItemReceipt {
|
|
46
|
+
readonly transfer_id: string;
|
|
47
|
+
readonly facility_id: string;
|
|
48
|
+
readonly sku: string;
|
|
49
|
+
readonly quantity_received: number;
|
|
50
|
+
readonly received_at: string;
|
|
51
|
+
}
|
|
52
|
+
export interface InforTransferOrder {
|
|
53
|
+
readonly subsidiary: {
|
|
54
|
+
readonly id: string;
|
|
55
|
+
};
|
|
56
|
+
readonly location: {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
};
|
|
59
|
+
readonly transferLocation: {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
};
|
|
62
|
+
readonly memo?: string;
|
|
63
|
+
readonly item: {
|
|
64
|
+
readonly items: ReadonlyArray<{
|
|
65
|
+
readonly item: {
|
|
66
|
+
readonly id: string;
|
|
67
|
+
};
|
|
68
|
+
readonly quantity: number;
|
|
69
|
+
readonly units?: {
|
|
70
|
+
readonly id: string;
|
|
71
|
+
};
|
|
72
|
+
}>;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface InforPurchaseOrder {
|
|
76
|
+
readonly entity: {
|
|
77
|
+
readonly id: string;
|
|
78
|
+
};
|
|
79
|
+
readonly subsidiary: {
|
|
80
|
+
readonly id: string;
|
|
81
|
+
};
|
|
82
|
+
readonly location: {
|
|
83
|
+
readonly id: string;
|
|
84
|
+
};
|
|
85
|
+
readonly memo?: string;
|
|
86
|
+
readonly item: {
|
|
87
|
+
readonly items: ReadonlyArray<{
|
|
88
|
+
readonly item: {
|
|
89
|
+
readonly id: string;
|
|
90
|
+
};
|
|
91
|
+
readonly quantity: number;
|
|
92
|
+
readonly rate?: number;
|
|
93
|
+
}>;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export interface InforInventoryBalance {
|
|
97
|
+
readonly subsidiary: {
|
|
98
|
+
readonly id: string;
|
|
99
|
+
readonly refName: string;
|
|
100
|
+
};
|
|
101
|
+
readonly location: {
|
|
102
|
+
readonly id: string;
|
|
103
|
+
readonly refName: string;
|
|
104
|
+
};
|
|
105
|
+
readonly item: {
|
|
106
|
+
readonly id: string;
|
|
107
|
+
readonly refName: string;
|
|
108
|
+
};
|
|
109
|
+
readonly quantityOnHand: number;
|
|
110
|
+
readonly quantityAvailable: number;
|
|
111
|
+
readonly lastModifiedDate: string;
|
|
112
|
+
}
|
|
113
|
+
export interface InforItemReceipt {
|
|
114
|
+
readonly createdFrom: {
|
|
115
|
+
readonly id: string;
|
|
116
|
+
};
|
|
117
|
+
readonly subsidiary: {
|
|
118
|
+
readonly id: string;
|
|
119
|
+
};
|
|
120
|
+
readonly item: {
|
|
121
|
+
readonly items: ReadonlyArray<{
|
|
122
|
+
readonly item: {
|
|
123
|
+
readonly id: string;
|
|
124
|
+
};
|
|
125
|
+
readonly quantity: number;
|
|
126
|
+
readonly location: {
|
|
127
|
+
readonly id: string;
|
|
128
|
+
};
|
|
129
|
+
}>;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export interface GovernanceConfig {
|
|
133
|
+
readonly read_only: boolean;
|
|
134
|
+
readonly audit_enabled: boolean;
|
|
135
|
+
readonly pii_redaction: boolean;
|
|
136
|
+
readonly max_tokens_per_request: number;
|
|
137
|
+
}
|
|
138
|
+
export interface SyncQueueEntry {
|
|
139
|
+
readonly id: string;
|
|
140
|
+
readonly operation: string;
|
|
141
|
+
readonly payload: unknown;
|
|
142
|
+
readonly created_at: string;
|
|
143
|
+
readonly retry_count: number;
|
|
144
|
+
readonly last_error: string | null;
|
|
145
|
+
}
|
|
146
|
+
export interface SyncStatus {
|
|
147
|
+
readonly last_successful_sync: string | null;
|
|
148
|
+
readonly pending_queue_depth: number;
|
|
149
|
+
readonly circuit_breaker_state: 'closed' | 'open' | 'half-open';
|
|
150
|
+
readonly recent_errors: ReadonlyArray<{
|
|
151
|
+
timestamp: string;
|
|
152
|
+
operation: string;
|
|
153
|
+
error: string;
|
|
154
|
+
}>;
|
|
155
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database client interface.
|
|
3
|
+
* Adapters depend on this abstraction — implementations are injected.
|
|
4
|
+
*/
|
|
5
|
+
export interface DbClient {
|
|
6
|
+
/** Execute a parameterized query and return rows. */
|
|
7
|
+
query(sql: string, params: unknown[]): Promise<unknown[]>;
|
|
8
|
+
/** Execute a parameterized statement (INSERT/UPDATE/DELETE). */
|
|
9
|
+
execute(sql: string, params: unknown[]): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* ERP client interface.
|
|
13
|
+
* Adapters delegate domain operations to the ERP system via this contract.
|
|
14
|
+
*/
|
|
15
|
+
export interface ErpClient {
|
|
16
|
+
/** Invoke a named ERP operation with a typed payload. */
|
|
17
|
+
invoke(operation: string, data: unknown): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Domain-specific error for adapter failures.
|
|
21
|
+
*/
|
|
22
|
+
export declare class AdapterError extends Error {
|
|
23
|
+
readonly context: string;
|
|
24
|
+
readonly operation: string;
|
|
25
|
+
readonly cause?: unknown | undefined;
|
|
26
|
+
constructor(message: string, context: string, operation: string, cause?: unknown | undefined);
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generated by Phase 4 pipeline — do not edit manually
|
|
2
|
+
/**
|
|
3
|
+
* Domain-specific error for adapter failures.
|
|
4
|
+
*/
|
|
5
|
+
export class AdapterError extends Error {
|
|
6
|
+
context;
|
|
7
|
+
operation;
|
|
8
|
+
cause;
|
|
9
|
+
constructor(message, context, operation, cause) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.context = context;
|
|
12
|
+
this.operation = operation;
|
|
13
|
+
this.cause = cause;
|
|
14
|
+
this.name = 'AdapterError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Pool Configuration
|
|
3
|
+
* ERP system: infor
|
|
4
|
+
* SQL dialect: oracle-sql
|
|
5
|
+
*/
|
|
6
|
+
export interface ConnectionPoolConfig {
|
|
7
|
+
readonly host: string;
|
|
8
|
+
readonly port: number;
|
|
9
|
+
readonly database: string;
|
|
10
|
+
readonly user: string;
|
|
11
|
+
readonly password: string;
|
|
12
|
+
readonly maxConnections: number;
|
|
13
|
+
readonly idleTimeoutMs: number;
|
|
14
|
+
readonly ssl: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function buildConnectionPoolConfig(): ConnectionPoolConfig;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Generated by Phase 4 pipeline — do not edit manually
|
|
2
|
+
export function buildConnectionPoolConfig() {
|
|
3
|
+
return {
|
|
4
|
+
host: process.env['DB_HOST'] ?? 'localhost',
|
|
5
|
+
port: Number(process.env['DB_PORT'] ?? 5432),
|
|
6
|
+
database: process.env['DB_NAME'] ?? 'infor',
|
|
7
|
+
user: process.env['DB_USER'] ?? '',
|
|
8
|
+
password: process.env['DB_PASSWORD'] ?? '',
|
|
9
|
+
maxConnections: Number(process.env['DB_MAX_CONNECTIONS'] ?? 10),
|
|
10
|
+
idleTimeoutMs: Number(process.env['DB_IDLE_TIMEOUT_MS'] ?? 30000),
|
|
11
|
+
ssl: process.env['DB_SSL'] !== 'false',
|
|
12
|
+
};
|
|
13
|
+
}
|