@finsys/core 2.6.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/dist/data/adapter-categories.json +142 -2
- package/dist/index.cjs +282 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +207 -23
- package/dist/index.d.ts +207 -23
- package/dist/index.js +279 -32
- package/dist/index.js.map +1 -1
- package/dist/schema/adapter-manifest.schema.json +13 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# @finsys/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The shared contract layer for the FinSys ecosystem. Two distinct surfaces:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1. **Unified form configuration** — generate Zod schemas, React Hook Form defaults, and SurveyJS JSON from a single JSON form spec.
|
|
6
|
+
2. **Source Adapter Framework** — vendor-agnostic contract for ingesting alternative-data sources (telco, payments, bank statements, social-media presence, etc.) into FinSys's canonical credit-signal vocabulary. Categories are a runtime registry loaded from a single data file (see [Category Reference](./docs/category-reference.md)).
|
|
7
|
+
|
|
8
|
+
## Building a Source Adapter?
|
|
9
|
+
|
|
10
|
+
Partner-facing integration documentation lives in [`docs/`](./docs/):
|
|
11
|
+
|
|
12
|
+
- 📖 [**Adapter Authoring Guide**](./docs/adapter-authoring.md) — start here. Walks from "what's an adapter" through "how do I package and deliver one."
|
|
13
|
+
- 📋 [Category Reference](./docs/category-reference.md) — canonical field set per category.
|
|
14
|
+
- 📋 [Manifest Reference](./docs/manifest-reference.md) — every field in `manifest.json`.
|
|
15
|
+
- 🔒 [Security Model](./docs/security-model.md) — sandboxing, secrets, network posture.
|
|
16
|
+
|
|
17
|
+
## Features (form-config surface)
|
|
6
18
|
|
|
7
19
|
- **Unified Form Configuration** — Single JSON format for defining form fields and multi-step page layouts
|
|
8
20
|
- **React Hook Form Generator** — Generate Zod schemas, default values, and step configurations from a form config
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{
|
|
5
5
|
"id": "telco-carrier",
|
|
6
6
|
"displayName": "Telco Carrier",
|
|
7
|
-
"description": "Mobile carrier bill-payment + account-history signals. Any
|
|
7
|
+
"description": "Mobile carrier bill-payment + account-history signals. Any mobile carrier implementing this category produces the same canonical field set.",
|
|
8
8
|
"canonicalTable": "ihs_alt_data_telco",
|
|
9
9
|
"fields": [
|
|
10
10
|
{
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
{
|
|
58
58
|
"id": "payment-network",
|
|
59
59
|
"displayName": "Payment Network",
|
|
60
|
-
"description": "Merchant-side payment-flow signals from POS / gateway networks
|
|
60
|
+
"description": "Merchant-side payment-flow signals from POS / payment-gateway networks. Captures actual transaction velocity rather than self-reported revenue.",
|
|
61
61
|
"canonicalTable": "ihs_alt_data_payments",
|
|
62
62
|
"fields": [
|
|
63
63
|
{
|
|
@@ -151,6 +151,146 @@
|
|
|
151
151
|
"description": "Bounced / returned transactions in the period. Distress signal at counts > 0."
|
|
152
152
|
}
|
|
153
153
|
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"id": "social-media",
|
|
157
|
+
"displayName": "Social Media Presence",
|
|
158
|
+
"description": "Public business-presence signals from social / commerce platforms — establishment, reach, engagement authenticity, customer reputation, and account standing. Vendor-agnostic: any platform exposing a public business profile maps its data to this canonical field set. Useful as thin-file corroboration of a borrower's operating reality where formal financials are sparse.",
|
|
159
|
+
"canonicalTable": "ihs_alt_data_social_media",
|
|
160
|
+
"fields": [
|
|
161
|
+
{
|
|
162
|
+
"name": "socialAccountTenureMonths",
|
|
163
|
+
"type": "number",
|
|
164
|
+
"unit": "months",
|
|
165
|
+
"range": [0, 600],
|
|
166
|
+
"description": "Age of the oldest verified public business presence across linked profiles. Establishment / continuity proxy, parallel to telco + payment-network tenure."
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"name": "socialFollowerCount",
|
|
170
|
+
"type": "number",
|
|
171
|
+
"unit": "count",
|
|
172
|
+
"range": [0, 100000000],
|
|
173
|
+
"description": "Aggregate audience size across linked public profiles. Coarse reach / scale proxy — gameable on its own, so read alongside socialEngagementRate90d."
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"name": "socialEngagementRate90d",
|
|
177
|
+
"type": "number",
|
|
178
|
+
"unit": "ratio",
|
|
179
|
+
"range": [0, 1],
|
|
180
|
+
"description": "Mean interactions per impression over the trailing 90 days. Authenticity signal: a large follower count with near-zero engagement indicates a bought or dormant audience."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "socialPostingConsistency12m",
|
|
184
|
+
"type": "number",
|
|
185
|
+
"unit": "ratio",
|
|
186
|
+
"range": [0, 1],
|
|
187
|
+
"description": "Fraction of weeks in the trailing 12 months with at least one public post. Ongoing-operation signal — distinguishes an active business from a stale listing."
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"name": "socialVerifiedBusinessAccount",
|
|
191
|
+
"type": "boolean",
|
|
192
|
+
"description": "Has at least one platform-verified business / commerce profile. Legitimacy signal — the platform has performed its own business-identity check."
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "socialCustomerRatingAvg",
|
|
196
|
+
"type": "number",
|
|
197
|
+
"unit": "rating",
|
|
198
|
+
"range": [0, 5],
|
|
199
|
+
"description": "Mean public customer rating (normalised to a 0–5 scale) across review-bearing profiles. Reputation signal; especially predictive for consumer-facing SMEs."
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "socialNegativeSentimentRatio90d",
|
|
203
|
+
"type": "number",
|
|
204
|
+
"unit": "ratio",
|
|
205
|
+
"range": [0, 1],
|
|
206
|
+
"description": "Fraction of public mentions / reviews classified as negative over the trailing 90 days. Reputation-risk / distress signal independent of overall rating volume."
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"name": "socialAccountFlags24m",
|
|
210
|
+
"type": "number",
|
|
211
|
+
"unit": "count",
|
|
212
|
+
"range": [0, 100],
|
|
213
|
+
"description": "Policy strikes, suspensions, or content takedowns across linked profiles in the last 24 months. Distress signal, parallel to telcoSuspensionsCount24m."
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"id": "trade-credit",
|
|
219
|
+
"displayName": "Trade Credit (Accounting)",
|
|
220
|
+
"description": "Accounts-receivable / accounts-payable aging and ledger-derived working-capital signals sourced from a business's accounting / ERP system. Captures how promptly the business collects from its debtors and pays its creditors, the aging profile and concentration of its receivables, and P&L / cash-conversion efficiency. Vendor-agnostic: any accounting or ERP system that exposes an AR/AP aging report plus a P&L summary maps its data to this canonical field set. A direct, high-signal view of trade-obligation behaviour — closer to formal credit history than most alternative data — and the anchor for cross-referencing self-reported accounting figures against bank-statement reality.",
|
|
221
|
+
"canonicalTable": "ihs_alt_data_trade_credit",
|
|
222
|
+
"fields": [
|
|
223
|
+
{
|
|
224
|
+
"name": "arDaysSalesOutstanding",
|
|
225
|
+
"type": "number",
|
|
226
|
+
"unit": "days",
|
|
227
|
+
"range": [0, 400],
|
|
228
|
+
"description": "Days Sales Outstanding — average days to collect a receivable. The headline collection-efficiency metric; lower is healthier, and a rising DSO is an early liquidity-stress signal."
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"name": "apDaysPayableOutstanding",
|
|
232
|
+
"type": "number",
|
|
233
|
+
"unit": "days",
|
|
234
|
+
"range": [0, 400],
|
|
235
|
+
"description": "Days Payable Outstanding — average days the business takes to pay its suppliers. Context for DSO; a very high DPO can indicate the business is stretching its creditors to fund operations."
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "arTotalOutstandingMyr",
|
|
239
|
+
"type": "number",
|
|
240
|
+
"unit": "MYR",
|
|
241
|
+
"range": [0, 1000000000],
|
|
242
|
+
"description": "Total accounts-receivable balance outstanding (RM). The size of the debtor book the business is carrying."
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"name": "arCurrentRatio",
|
|
246
|
+
"type": "number",
|
|
247
|
+
"unit": "ratio",
|
|
248
|
+
"range": [0, 1],
|
|
249
|
+
"description": "Fraction of the receivables book that is current (within payment terms, not yet overdue). Higher is healthier; the complement is the overdue share across all aging buckets."
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"name": "arOverdue90PlusRatio",
|
|
253
|
+
"type": "number",
|
|
254
|
+
"unit": "ratio",
|
|
255
|
+
"range": [0, 1],
|
|
256
|
+
"description": "Fraction of the receivables book aged more than 90 days past terms. The key impairment / bad-debt risk signal from the AR aging report; elevated values indicate collection difficulty."
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"name": "debtorConcentrationTop5Ratio",
|
|
260
|
+
"type": "number",
|
|
261
|
+
"unit": "ratio",
|
|
262
|
+
"range": [0, 1],
|
|
263
|
+
"description": "Share of total receivables owed by the top-5 debtors. Above ~0.6 is single-customer concentration risk — one debtor default would materially impair cash flow."
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"name": "tradeReferenceDefaults12m",
|
|
267
|
+
"type": "number",
|
|
268
|
+
"unit": "count",
|
|
269
|
+
"range": [0, 1000],
|
|
270
|
+
"description": "Count of supplier-reported defaults or dishonoured / returned payments against the business in the trailing 12 months. Direct distress signal, parallel to a bureau default record."
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"name": "accountingRevenue12mMyr",
|
|
274
|
+
"type": "number",
|
|
275
|
+
"unit": "MYR",
|
|
276
|
+
"range": [0, 1000000000],
|
|
277
|
+
"description": "Trailing-12-month turnover per the general ledger (RM). The cross-reference anchor: comparing this self-reported accounting revenue against payment-network volume and bank-statement inflows surfaces book-vs-reality inconsistencies."
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"name": "grossMarginPct",
|
|
281
|
+
"type": "number",
|
|
282
|
+
"unit": "ratio",
|
|
283
|
+
"range": [0, 1],
|
|
284
|
+
"description": "Gross margin from the P&L (gross profit / revenue). Profitability-quality signal; a thin or declining margin constrains debt-service capacity even at healthy revenue."
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"name": "cashConversionCycleDays",
|
|
288
|
+
"type": "number",
|
|
289
|
+
"unit": "days",
|
|
290
|
+
"range": [-200, 600],
|
|
291
|
+
"description": "Cash Conversion Cycle (DSO + days-inventory-outstanding − DPO). Working-capital efficiency; negative values (collect before paying suppliers) are strongest, very high values indicate cash tied up in operations."
|
|
292
|
+
}
|
|
293
|
+
]
|
|
154
294
|
}
|
|
155
295
|
]
|
|
156
296
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
ADAPTER_CATEGORY_IDS: () => ADAPTER_CATEGORY_IDS,
|
|
33
34
|
ALL_AGGREGATION_OPS: () => ALL_AGGREGATION_OPS,
|
|
34
35
|
AdapterError: () => AdapterError,
|
|
35
36
|
BASE_FIELD_SPECS: () => BASE_FIELD_SPECS,
|
|
@@ -55,6 +56,7 @@ __export(index_exports, {
|
|
|
55
56
|
allCategories: () => allCategories,
|
|
56
57
|
applyAggregation: () => applyAggregation,
|
|
57
58
|
applyDynamicTitles: () => applyDynamicTitles,
|
|
59
|
+
assertAdapterCategory: () => assertAdapterCategory,
|
|
58
60
|
buildFileFieldTables: () => buildFileFieldTables,
|
|
59
61
|
categoryFieldsOf: () => categoryFieldsOf,
|
|
60
62
|
categoryForField: () => categoryForField,
|
|
@@ -79,6 +81,7 @@ __export(index_exports, {
|
|
|
79
81
|
groupDetailsByCategory: () => groupDetailsByCategory,
|
|
80
82
|
groupFieldsByCategory: () => groupFieldsByCategory,
|
|
81
83
|
groupFieldsByPattern: () => groupFieldsByPattern,
|
|
84
|
+
isAdapterCategory: () => isAdapterCategory,
|
|
82
85
|
isFailureIhsStatus: () => isFailureIhsStatus,
|
|
83
86
|
isTerminalIhsStatus: () => isTerminalIhsStatus,
|
|
84
87
|
isValidIhsStatus: () => isValidIhsStatus,
|
|
@@ -102,7 +105,7 @@ function getPastYearLabel(yearsAgo) {
|
|
|
102
105
|
const date = /* @__PURE__ */ new Date();
|
|
103
106
|
return (date.getFullYear() - yearsAgo).toString();
|
|
104
107
|
}
|
|
105
|
-
function evaluateExpression(expression,
|
|
108
|
+
function evaluateExpression(expression, data) {
|
|
106
109
|
if (!expression) return true;
|
|
107
110
|
try {
|
|
108
111
|
let jsExpression = expression.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
@@ -126,7 +129,7 @@ function evaluateExpression(expression, data2) {
|
|
|
126
129
|
(_, ref, val) => `!(${ref} || []).includes('${val}')`
|
|
127
130
|
);
|
|
128
131
|
const func = new Function(`return ${jsExpression};`);
|
|
129
|
-
return !!func.call(
|
|
132
|
+
return !!func.call(data || {});
|
|
130
133
|
} catch (e) {
|
|
131
134
|
return false;
|
|
132
135
|
}
|
|
@@ -272,16 +275,16 @@ function groupFieldsByCategory(fields, categories) {
|
|
|
272
275
|
// src/rhf-generator.ts
|
|
273
276
|
var import_zod = require("zod");
|
|
274
277
|
function applyVisibilityValidation(baseObject, fields) {
|
|
275
|
-
return baseObject.superRefine((
|
|
278
|
+
return baseObject.superRefine((data, ctx) => {
|
|
276
279
|
fields.forEach((field) => {
|
|
277
280
|
const isRequired = field.isRequired !== false && field.required !== false && field.type !== "html";
|
|
278
281
|
if (!isRequired) return;
|
|
279
282
|
let isVisible = field.visible !== false;
|
|
280
283
|
if (isVisible && field.visibleIf) {
|
|
281
|
-
isVisible = evaluateExpression(field.visibleIf,
|
|
284
|
+
isVisible = evaluateExpression(field.visibleIf, data);
|
|
282
285
|
}
|
|
283
286
|
if (isVisible) {
|
|
284
|
-
const value =
|
|
287
|
+
const value = data[field.name];
|
|
285
288
|
const isEmpty = value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
|
|
286
289
|
if (isEmpty) {
|
|
287
290
|
ctx.addIssue({
|
|
@@ -742,22 +745,22 @@ var ajv = new Ajv({ allErrors: true, strict: false });
|
|
|
742
745
|
var addFormatsFn = import_ajv_formats.default.default || import_ajv_formats.default;
|
|
743
746
|
addFormatsFn(ajv);
|
|
744
747
|
ajv.addSchema(unified_form_schema_default, "unified-form.schema.json");
|
|
745
|
-
function validateFormConfig(
|
|
748
|
+
function validateFormConfig(data) {
|
|
746
749
|
const validate = ajv.getSchema("unified-form.schema.json");
|
|
747
750
|
if (!validate) {
|
|
748
751
|
return { valid: false, message: "Could not load unified form schema" };
|
|
749
752
|
}
|
|
750
|
-
const valid = validate(
|
|
753
|
+
const valid = validate(data);
|
|
751
754
|
return {
|
|
752
755
|
valid: !!valid,
|
|
753
756
|
errors: validate.errors || void 0
|
|
754
757
|
};
|
|
755
758
|
}
|
|
756
|
-
function validateFormSpec(
|
|
757
|
-
return validateFormConfig(
|
|
759
|
+
function validateFormSpec(data) {
|
|
760
|
+
return validateFormConfig(data);
|
|
758
761
|
}
|
|
759
|
-
function validatePagesConfig(
|
|
760
|
-
return validateFormConfig(
|
|
762
|
+
function validatePagesConfig(data) {
|
|
763
|
+
return validateFormConfig(data);
|
|
761
764
|
}
|
|
762
765
|
|
|
763
766
|
// src/form-field-category.ts
|
|
@@ -1214,10 +1217,10 @@ var _FormSpec = class _FormSpec {
|
|
|
1214
1217
|
if (Array.isArray(jsonData.fields)) {
|
|
1215
1218
|
fieldObjects = jsonData.fields;
|
|
1216
1219
|
} else if (typeof jsonData.fields === "object" && jsonData.fields !== null) {
|
|
1217
|
-
fieldObjects = Object.entries(jsonData.fields).map(([name,
|
|
1220
|
+
fieldObjects = Object.entries(jsonData.fields).map(([name, data]) => {
|
|
1218
1221
|
return {
|
|
1219
1222
|
name,
|
|
1220
|
-
...
|
|
1223
|
+
...data
|
|
1221
1224
|
};
|
|
1222
1225
|
});
|
|
1223
1226
|
}
|
|
@@ -9475,18 +9478,18 @@ function buildTableForGroup(groupName, fields, ihsData) {
|
|
|
9475
9478
|
const items = [];
|
|
9476
9479
|
for (const [baseName, periodMap] of Object.entries(columnGroups)) {
|
|
9477
9480
|
const numeric = isNumericField(baseName);
|
|
9478
|
-
const
|
|
9481
|
+
const data = {};
|
|
9479
9482
|
const formattedData = {};
|
|
9480
9483
|
for (const period of periods) {
|
|
9481
9484
|
const colName = periodMap[period];
|
|
9482
9485
|
const value = colName ? ihsData[colName] ?? null : null;
|
|
9483
|
-
|
|
9486
|
+
data[period] = value;
|
|
9484
9487
|
formattedData[period] = formatValue(value, numeric);
|
|
9485
9488
|
}
|
|
9486
9489
|
items.push({
|
|
9487
9490
|
displayName: getDisplayName(baseName),
|
|
9488
9491
|
timePeriods: periods,
|
|
9489
|
-
data
|
|
9492
|
+
data,
|
|
9490
9493
|
formattedData,
|
|
9491
9494
|
type: tableType,
|
|
9492
9495
|
isNumeric: numeric
|
|
@@ -9907,7 +9910,7 @@ var adapter_categories_default = {
|
|
|
9907
9910
|
{
|
|
9908
9911
|
id: "telco-carrier",
|
|
9909
9912
|
displayName: "Telco Carrier",
|
|
9910
|
-
description: "Mobile carrier bill-payment + account-history signals. Any
|
|
9913
|
+
description: "Mobile carrier bill-payment + account-history signals. Any mobile carrier implementing this category produces the same canonical field set.",
|
|
9911
9914
|
canonicalTable: "ihs_alt_data_telco",
|
|
9912
9915
|
fields: [
|
|
9913
9916
|
{
|
|
@@ -9960,7 +9963,7 @@ var adapter_categories_default = {
|
|
|
9960
9963
|
{
|
|
9961
9964
|
id: "payment-network",
|
|
9962
9965
|
displayName: "Payment Network",
|
|
9963
|
-
description: "Merchant-side payment-flow signals from POS / gateway networks
|
|
9966
|
+
description: "Merchant-side payment-flow signals from POS / payment-gateway networks. Captures actual transaction velocity rather than self-reported revenue.",
|
|
9964
9967
|
canonicalTable: "ihs_alt_data_payments",
|
|
9965
9968
|
fields: [
|
|
9966
9969
|
{
|
|
@@ -10054,17 +10057,259 @@ var adapter_categories_default = {
|
|
|
10054
10057
|
description: "Bounced / returned transactions in the period. Distress signal at counts > 0."
|
|
10055
10058
|
}
|
|
10056
10059
|
]
|
|
10060
|
+
},
|
|
10061
|
+
{
|
|
10062
|
+
id: "social-media",
|
|
10063
|
+
displayName: "Social Media Presence",
|
|
10064
|
+
description: "Public business-presence signals from social / commerce platforms \u2014 establishment, reach, engagement authenticity, customer reputation, and account standing. Vendor-agnostic: any platform exposing a public business profile maps its data to this canonical field set. Useful as thin-file corroboration of a borrower's operating reality where formal financials are sparse.",
|
|
10065
|
+
canonicalTable: "ihs_alt_data_social_media",
|
|
10066
|
+
fields: [
|
|
10067
|
+
{
|
|
10068
|
+
name: "socialAccountTenureMonths",
|
|
10069
|
+
type: "number",
|
|
10070
|
+
unit: "months",
|
|
10071
|
+
range: [0, 600],
|
|
10072
|
+
description: "Age of the oldest verified public business presence across linked profiles. Establishment / continuity proxy, parallel to telco + payment-network tenure."
|
|
10073
|
+
},
|
|
10074
|
+
{
|
|
10075
|
+
name: "socialFollowerCount",
|
|
10076
|
+
type: "number",
|
|
10077
|
+
unit: "count",
|
|
10078
|
+
range: [0, 1e8],
|
|
10079
|
+
description: "Aggregate audience size across linked public profiles. Coarse reach / scale proxy \u2014 gameable on its own, so read alongside socialEngagementRate90d."
|
|
10080
|
+
},
|
|
10081
|
+
{
|
|
10082
|
+
name: "socialEngagementRate90d",
|
|
10083
|
+
type: "number",
|
|
10084
|
+
unit: "ratio",
|
|
10085
|
+
range: [0, 1],
|
|
10086
|
+
description: "Mean interactions per impression over the trailing 90 days. Authenticity signal: a large follower count with near-zero engagement indicates a bought or dormant audience."
|
|
10087
|
+
},
|
|
10088
|
+
{
|
|
10089
|
+
name: "socialPostingConsistency12m",
|
|
10090
|
+
type: "number",
|
|
10091
|
+
unit: "ratio",
|
|
10092
|
+
range: [0, 1],
|
|
10093
|
+
description: "Fraction of weeks in the trailing 12 months with at least one public post. Ongoing-operation signal \u2014 distinguishes an active business from a stale listing."
|
|
10094
|
+
},
|
|
10095
|
+
{
|
|
10096
|
+
name: "socialVerifiedBusinessAccount",
|
|
10097
|
+
type: "boolean",
|
|
10098
|
+
description: "Has at least one platform-verified business / commerce profile. Legitimacy signal \u2014 the platform has performed its own business-identity check."
|
|
10099
|
+
},
|
|
10100
|
+
{
|
|
10101
|
+
name: "socialCustomerRatingAvg",
|
|
10102
|
+
type: "number",
|
|
10103
|
+
unit: "rating",
|
|
10104
|
+
range: [0, 5],
|
|
10105
|
+
description: "Mean public customer rating (normalised to a 0\u20135 scale) across review-bearing profiles. Reputation signal; especially predictive for consumer-facing SMEs."
|
|
10106
|
+
},
|
|
10107
|
+
{
|
|
10108
|
+
name: "socialNegativeSentimentRatio90d",
|
|
10109
|
+
type: "number",
|
|
10110
|
+
unit: "ratio",
|
|
10111
|
+
range: [0, 1],
|
|
10112
|
+
description: "Fraction of public mentions / reviews classified as negative over the trailing 90 days. Reputation-risk / distress signal independent of overall rating volume."
|
|
10113
|
+
},
|
|
10114
|
+
{
|
|
10115
|
+
name: "socialAccountFlags24m",
|
|
10116
|
+
type: "number",
|
|
10117
|
+
unit: "count",
|
|
10118
|
+
range: [0, 100],
|
|
10119
|
+
description: "Policy strikes, suspensions, or content takedowns across linked profiles in the last 24 months. Distress signal, parallel to telcoSuspensionsCount24m."
|
|
10120
|
+
}
|
|
10121
|
+
]
|
|
10122
|
+
},
|
|
10123
|
+
{
|
|
10124
|
+
id: "trade-credit",
|
|
10125
|
+
displayName: "Trade Credit (Accounting)",
|
|
10126
|
+
description: "Accounts-receivable / accounts-payable aging and ledger-derived working-capital signals sourced from a business's accounting / ERP system. Captures how promptly the business collects from its debtors and pays its creditors, the aging profile and concentration of its receivables, and P&L / cash-conversion efficiency. Vendor-agnostic: any accounting or ERP system that exposes an AR/AP aging report plus a P&L summary maps its data to this canonical field set. A direct, high-signal view of trade-obligation behaviour \u2014 closer to formal credit history than most alternative data \u2014 and the anchor for cross-referencing self-reported accounting figures against bank-statement reality.",
|
|
10127
|
+
canonicalTable: "ihs_alt_data_trade_credit",
|
|
10128
|
+
fields: [
|
|
10129
|
+
{
|
|
10130
|
+
name: "arDaysSalesOutstanding",
|
|
10131
|
+
type: "number",
|
|
10132
|
+
unit: "days",
|
|
10133
|
+
range: [0, 400],
|
|
10134
|
+
description: "Days Sales Outstanding \u2014 average days to collect a receivable. The headline collection-efficiency metric; lower is healthier, and a rising DSO is an early liquidity-stress signal."
|
|
10135
|
+
},
|
|
10136
|
+
{
|
|
10137
|
+
name: "apDaysPayableOutstanding",
|
|
10138
|
+
type: "number",
|
|
10139
|
+
unit: "days",
|
|
10140
|
+
range: [0, 400],
|
|
10141
|
+
description: "Days Payable Outstanding \u2014 average days the business takes to pay its suppliers. Context for DSO; a very high DPO can indicate the business is stretching its creditors to fund operations."
|
|
10142
|
+
},
|
|
10143
|
+
{
|
|
10144
|
+
name: "arTotalOutstandingMyr",
|
|
10145
|
+
type: "number",
|
|
10146
|
+
unit: "MYR",
|
|
10147
|
+
range: [0, 1e9],
|
|
10148
|
+
description: "Total accounts-receivable balance outstanding (RM). The size of the debtor book the business is carrying."
|
|
10149
|
+
},
|
|
10150
|
+
{
|
|
10151
|
+
name: "arCurrentRatio",
|
|
10152
|
+
type: "number",
|
|
10153
|
+
unit: "ratio",
|
|
10154
|
+
range: [0, 1],
|
|
10155
|
+
description: "Fraction of the receivables book that is current (within payment terms, not yet overdue). Higher is healthier; the complement is the overdue share across all aging buckets."
|
|
10156
|
+
},
|
|
10157
|
+
{
|
|
10158
|
+
name: "arOverdue90PlusRatio",
|
|
10159
|
+
type: "number",
|
|
10160
|
+
unit: "ratio",
|
|
10161
|
+
range: [0, 1],
|
|
10162
|
+
description: "Fraction of the receivables book aged more than 90 days past terms. The key impairment / bad-debt risk signal from the AR aging report; elevated values indicate collection difficulty."
|
|
10163
|
+
},
|
|
10164
|
+
{
|
|
10165
|
+
name: "debtorConcentrationTop5Ratio",
|
|
10166
|
+
type: "number",
|
|
10167
|
+
unit: "ratio",
|
|
10168
|
+
range: [0, 1],
|
|
10169
|
+
description: "Share of total receivables owed by the top-5 debtors. Above ~0.6 is single-customer concentration risk \u2014 one debtor default would materially impair cash flow."
|
|
10170
|
+
},
|
|
10171
|
+
{
|
|
10172
|
+
name: "tradeReferenceDefaults12m",
|
|
10173
|
+
type: "number",
|
|
10174
|
+
unit: "count",
|
|
10175
|
+
range: [0, 1e3],
|
|
10176
|
+
description: "Count of supplier-reported defaults or dishonoured / returned payments against the business in the trailing 12 months. Direct distress signal, parallel to a bureau default record."
|
|
10177
|
+
},
|
|
10178
|
+
{
|
|
10179
|
+
name: "accountingRevenue12mMyr",
|
|
10180
|
+
type: "number",
|
|
10181
|
+
unit: "MYR",
|
|
10182
|
+
range: [0, 1e9],
|
|
10183
|
+
description: "Trailing-12-month turnover per the general ledger (RM). The cross-reference anchor: comparing this self-reported accounting revenue against payment-network volume and bank-statement inflows surfaces book-vs-reality inconsistencies."
|
|
10184
|
+
},
|
|
10185
|
+
{
|
|
10186
|
+
name: "grossMarginPct",
|
|
10187
|
+
type: "number",
|
|
10188
|
+
unit: "ratio",
|
|
10189
|
+
range: [0, 1],
|
|
10190
|
+
description: "Gross margin from the P&L (gross profit / revenue). Profitability-quality signal; a thin or declining margin constrains debt-service capacity even at healthy revenue."
|
|
10191
|
+
},
|
|
10192
|
+
{
|
|
10193
|
+
name: "cashConversionCycleDays",
|
|
10194
|
+
type: "number",
|
|
10195
|
+
unit: "days",
|
|
10196
|
+
range: [-200, 600],
|
|
10197
|
+
description: "Cash Conversion Cycle (DSO + days-inventory-outstanding \u2212 DPO). Working-capital efficiency; negative values (collect before paying suppliers) are strongest, very high values indicate cash tied up in operations."
|
|
10198
|
+
}
|
|
10199
|
+
]
|
|
10057
10200
|
}
|
|
10058
10201
|
]
|
|
10059
10202
|
};
|
|
10060
10203
|
|
|
10061
10204
|
// src/adapter-categories.ts
|
|
10062
|
-
var
|
|
10205
|
+
var VALID_FIELD_TYPES = [
|
|
10206
|
+
"number",
|
|
10207
|
+
"boolean",
|
|
10208
|
+
"string"
|
|
10209
|
+
];
|
|
10210
|
+
function buildCategoryRegistry(raw) {
|
|
10211
|
+
if (!raw || typeof raw !== "object") {
|
|
10212
|
+
throw new Error("adapter category data: expected an object");
|
|
10213
|
+
}
|
|
10214
|
+
if (typeof raw.schemaVersion !== "string" || raw.schemaVersion.length === 0) {
|
|
10215
|
+
throw new Error("adapter category data: missing schemaVersion");
|
|
10216
|
+
}
|
|
10217
|
+
if (!Array.isArray(raw.categories) || raw.categories.length === 0) {
|
|
10218
|
+
throw new Error("adapter category data: categories must be a non-empty array");
|
|
10219
|
+
}
|
|
10220
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10221
|
+
const fieldToCategory = /* @__PURE__ */ new Map();
|
|
10222
|
+
const tables = /* @__PURE__ */ new Set();
|
|
10223
|
+
const all = [];
|
|
10224
|
+
for (const cat of raw.categories) {
|
|
10225
|
+
const where = `category "${cat?.id ?? "<unknown>"}"`;
|
|
10226
|
+
if (typeof cat.id !== "string" || cat.id.length === 0) {
|
|
10227
|
+
throw new Error("adapter category data: every category needs a non-empty id");
|
|
10228
|
+
}
|
|
10229
|
+
if (byId.has(cat.id)) {
|
|
10230
|
+
throw new Error(`adapter category data: duplicate category id "${cat.id}"`);
|
|
10231
|
+
}
|
|
10232
|
+
if (typeof cat.displayName !== "string" || cat.displayName.length === 0) {
|
|
10233
|
+
throw new Error(`adapter category data: ${where} needs a non-empty displayName`);
|
|
10234
|
+
}
|
|
10235
|
+
if (typeof cat.description !== "string" || cat.description.length === 0) {
|
|
10236
|
+
throw new Error(`adapter category data: ${where} needs a non-empty description`);
|
|
10237
|
+
}
|
|
10238
|
+
if (typeof cat.canonicalTable !== "string" || !/^ihs_alt_data_/.test(cat.canonicalTable)) {
|
|
10239
|
+
throw new Error(
|
|
10240
|
+
`adapter category data: ${where} canonicalTable must start with "ihs_alt_data_" (got "${cat.canonicalTable}")`
|
|
10241
|
+
);
|
|
10242
|
+
}
|
|
10243
|
+
if (tables.has(cat.canonicalTable)) {
|
|
10244
|
+
throw new Error(
|
|
10245
|
+
`adapter category data: duplicate canonicalTable "${cat.canonicalTable}" (${where})`
|
|
10246
|
+
);
|
|
10247
|
+
}
|
|
10248
|
+
if (!Array.isArray(cat.fields) || cat.fields.length === 0) {
|
|
10249
|
+
throw new Error(`adapter category data: ${where} must declare at least one field`);
|
|
10250
|
+
}
|
|
10251
|
+
const fields = [];
|
|
10252
|
+
for (const f of cat.fields) {
|
|
10253
|
+
if (typeof f.name !== "string" || f.name.length === 0) {
|
|
10254
|
+
throw new Error(`adapter category data: ${where} has a field with no name`);
|
|
10255
|
+
}
|
|
10256
|
+
if (fieldToCategory.has(f.name)) {
|
|
10257
|
+
throw new Error(
|
|
10258
|
+
`adapter category data: canonical field "${f.name}" declared by more than one category (${fieldToCategory.get(f.name)} + ${cat.id}) \u2014 field names must be globally unique`
|
|
10259
|
+
);
|
|
10260
|
+
}
|
|
10261
|
+
if (!VALID_FIELD_TYPES.includes(f.type)) {
|
|
10262
|
+
throw new Error(
|
|
10263
|
+
`adapter category data: field "${f.name}" (${where}) has invalid type "${f.type}"`
|
|
10264
|
+
);
|
|
10265
|
+
}
|
|
10266
|
+
if (typeof f.description !== "string" || f.description.length === 0) {
|
|
10267
|
+
throw new Error(
|
|
10268
|
+
`adapter category data: field "${f.name}" (${where}) needs a non-empty description`
|
|
10269
|
+
);
|
|
10270
|
+
}
|
|
10271
|
+
if (f.range !== void 0) {
|
|
10272
|
+
if (!Array.isArray(f.range) || f.range.length !== 2 || !Number.isFinite(f.range[0]) || !Number.isFinite(f.range[1]) || f.range[0] > f.range[1]) {
|
|
10273
|
+
throw new Error(
|
|
10274
|
+
`adapter category data: field "${f.name}" (${where}) has an invalid range \u2014 expected [lo, hi] with finite lo <= hi`
|
|
10275
|
+
);
|
|
10276
|
+
}
|
|
10277
|
+
}
|
|
10278
|
+
const spec = Object.freeze({
|
|
10279
|
+
name: f.name,
|
|
10280
|
+
type: f.type,
|
|
10281
|
+
...f.unit !== void 0 ? { unit: f.unit } : {},
|
|
10282
|
+
...f.range !== void 0 ? { range: Object.freeze([f.range[0], f.range[1]]) } : {},
|
|
10283
|
+
description: f.description
|
|
10284
|
+
});
|
|
10285
|
+
fields.push(spec);
|
|
10286
|
+
fieldToCategory.set(f.name, cat.id);
|
|
10287
|
+
}
|
|
10288
|
+
const schema = Object.freeze({
|
|
10289
|
+
id: cat.id,
|
|
10290
|
+
displayName: cat.displayName,
|
|
10291
|
+
description: cat.description,
|
|
10292
|
+
canonicalTable: cat.canonicalTable,
|
|
10293
|
+
fields: Object.freeze(fields)
|
|
10294
|
+
});
|
|
10295
|
+
byId.set(cat.id, schema);
|
|
10296
|
+
tables.add(cat.canonicalTable);
|
|
10297
|
+
all.push(schema);
|
|
10298
|
+
}
|
|
10299
|
+
return Object.freeze({
|
|
10300
|
+
all: Object.freeze(all),
|
|
10301
|
+
ids: Object.freeze(all.map((c) => c.id)),
|
|
10302
|
+
byId,
|
|
10303
|
+
fieldToCategory
|
|
10304
|
+
});
|
|
10305
|
+
}
|
|
10306
|
+
var registry = buildCategoryRegistry(adapter_categories_default);
|
|
10307
|
+
var ADAPTER_CATEGORY_IDS = registry.ids;
|
|
10063
10308
|
function categorySchemaOf(id) {
|
|
10064
|
-
const found =
|
|
10309
|
+
const found = registry.byId.get(id);
|
|
10065
10310
|
if (!found) {
|
|
10066
10311
|
throw new Error(
|
|
10067
|
-
`Unknown adapter category: ${id}. Available: ${
|
|
10312
|
+
`Unknown adapter category: ${id}. Available: ${registry.ids.join(", ")}`
|
|
10068
10313
|
);
|
|
10069
10314
|
}
|
|
10070
10315
|
return found;
|
|
@@ -10073,22 +10318,25 @@ function categoryFieldsOf(id) {
|
|
|
10073
10318
|
return categorySchemaOf(id).fields.map((f) => f.name);
|
|
10074
10319
|
}
|
|
10075
10320
|
function allCategories() {
|
|
10076
|
-
return
|
|
10321
|
+
return registry.all;
|
|
10077
10322
|
}
|
|
10078
|
-
var fieldToCategory = (() => {
|
|
10079
|
-
const m = /* @__PURE__ */ new Map();
|
|
10080
|
-
for (const cat of data.categories) {
|
|
10081
|
-
for (const f of cat.fields) {
|
|
10082
|
-
m.set(f.name, cat.id);
|
|
10083
|
-
}
|
|
10084
|
-
}
|
|
10085
|
-
return m;
|
|
10086
|
-
})();
|
|
10087
10323
|
function categoryForField(field) {
|
|
10088
|
-
return fieldToCategory.get(field) ?? null;
|
|
10324
|
+
return registry.fieldToCategory.get(field) ?? null;
|
|
10325
|
+
}
|
|
10326
|
+
function isAdapterCategory(id) {
|
|
10327
|
+
return registry.byId.has(id);
|
|
10328
|
+
}
|
|
10329
|
+
function assertAdapterCategory(id) {
|
|
10330
|
+
if (!registry.byId.has(id)) {
|
|
10331
|
+
throw new Error(
|
|
10332
|
+
`Unknown adapter category: ${id}. Available: ${registry.ids.join(", ")}`
|
|
10333
|
+
);
|
|
10334
|
+
}
|
|
10335
|
+
return id;
|
|
10089
10336
|
}
|
|
10090
10337
|
// Annotate the CommonJS export names for ESM import in node:
|
|
10091
10338
|
0 && (module.exports = {
|
|
10339
|
+
ADAPTER_CATEGORY_IDS,
|
|
10092
10340
|
ALL_AGGREGATION_OPS,
|
|
10093
10341
|
AdapterError,
|
|
10094
10342
|
BASE_FIELD_SPECS,
|
|
@@ -10114,6 +10362,7 @@ function categoryForField(field) {
|
|
|
10114
10362
|
allCategories,
|
|
10115
10363
|
applyAggregation,
|
|
10116
10364
|
applyDynamicTitles,
|
|
10365
|
+
assertAdapterCategory,
|
|
10117
10366
|
buildFileFieldTables,
|
|
10118
10367
|
categoryFieldsOf,
|
|
10119
10368
|
categoryForField,
|
|
@@ -10138,6 +10387,7 @@ function categoryForField(field) {
|
|
|
10138
10387
|
groupDetailsByCategory,
|
|
10139
10388
|
groupFieldsByCategory,
|
|
10140
10389
|
groupFieldsByPattern,
|
|
10390
|
+
isAdapterCategory,
|
|
10141
10391
|
isFailureIhsStatus,
|
|
10142
10392
|
isTerminalIhsStatus,
|
|
10143
10393
|
isValidIhsStatus,
|