@cosyte/x12 0.0.12 → 0.0.14
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/CHANGELOG.md +398 -2
- package/KNOWN-LIMITATIONS.md +313 -30
- package/README.md +2 -2
- package/dist/index.cjs +258 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +437 -51
- package/dist/index.d.ts +437 -51
- package/dist/index.mjs +253 -75
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/KNOWN-LIMITATIONS.md
CHANGED
|
@@ -16,6 +16,52 @@ model.
|
|
|
16
16
|
|
|
17
17
|
## Data / decode boundaries
|
|
18
18
|
|
|
19
|
+
- **🩺 Which `ST-03` implementation-convention references resolve to an 837 variant CHANGED in this
|
|
20
|
+
release, and some already-published files therefore decode differently** (`X12-VARIANT-ICR-UNGROUNDED`).
|
|
21
|
+
Through `0.0.13` `get837Claims` recognised exactly three references: `005010X222A2`, `005010X223A3`
|
|
22
|
+
and `005010X224A2`. **That set contained none of the identifiers HIPAA adopts at 45 CFR 162.1102**
|
|
23
|
+
(`005010X222`, `005010X223` + `005010X223A1`, `005010X224` + `005010X224A1`), **and it was missing
|
|
24
|
+
`005010X222A1` and `005010X223A2`**, which CMS and state Medicaid companion guides require in ST-03
|
|
25
|
+
and GS-08 on production professional and institutional claims. A conformant 837P declaring
|
|
26
|
+
`005010X222A1` therefore resolved to no variant at all and fell through to the `SVx` scan, where a
|
|
27
|
+
single stray `SV2` anywhere in the body re-typed the whole submission, and
|
|
28
|
+
`X12_837_UNKNOWN_VARIANT` accused a document that was not non-conformant. The reader now recognises
|
|
29
|
+
each base guide and each of its published errata. Sources for every key are named beside the table
|
|
30
|
+
in `src/transactions/claim/get-837.ts` and in
|
|
31
|
+
`documentation/agent-notes/x12-variant-icr-ungrounded.md`.
|
|
32
|
+
- **🛑 What this changes for a consumer, and it is a behaviour change on already-published
|
|
33
|
+
decoding.** On a file whose `ST-03` is now recognised: `submission.variant` can differ from what
|
|
34
|
+
`0.0.13` read, where the first `SVx` in the body disagreed with the declaration; and
|
|
35
|
+
**`X12_837_AMBIGUOUS_VARIANT` and `X12_837_UNKNOWN_VARIANT` no longer fire at all**, because no
|
|
36
|
+
guess was made. A predicate written against either code goes quiet on such a file. **🩺 And a
|
|
37
|
+
service line whose `SVx` kind disagrees with the declaration is no longer DECODED, so a code
|
|
38
|
+
STARTS firing on a document that may have carried `warnings: []`**: under `005010X222A1` with a
|
|
39
|
+
body whose only service segment is an `SV2`, `0.0.13` read `variant` `"I"` and `charge` `7300`
|
|
40
|
+
with no warnings, and this release reads `variant` `"P"`, `charge` `undefined`, and
|
|
41
|
+
`X12_837_SERVICE_LINE_NOT_DECODED` at that line's `LX`. A mis-stamped envelope is an ordinary
|
|
42
|
+
vendor variant and this reader cannot tell one from a conformant document; the loss is warned
|
|
43
|
+
rather than silent. **Read all of this as ONE property and never as a closed list: where `ST-03`
|
|
44
|
+
is now recognised the document's own declaration decides the variant instead of its first
|
|
45
|
+
service segment, and everything downstream follows from that.** Re-check any routing driven off
|
|
46
|
+
`submission.variant` for 837s read on `0.0.13` or earlier.
|
|
47
|
+
- **🛑 The `SVx` fallback is NOT narrowed.** First-wins still takes the first service segment in the
|
|
48
|
+
body, orphans included, on every document that still reaches it. What changed is which documents
|
|
49
|
+
reach it.
|
|
50
|
+
- **It is a LIST of cited identifiers, never a pattern.** A reference outside the set, in a
|
|
51
|
+
different case, or carrying leading whitespace, still falls through exactly as before. Nothing
|
|
52
|
+
trims, lower-cases or prefix-matches `ST-03`, because no source says to.
|
|
53
|
+
- **The set is not claimed exhaustive and no count of it is published**, here or in any warning
|
|
54
|
+
message. Both variant messages named the three old keys literally and were wrong the moment the
|
|
55
|
+
table was corrected, so neither enumerates the set any more.
|
|
56
|
+
- **🩺 OPEN, and NOT fixed here: the EMIT side still stamps the old three.** `build837P` /
|
|
57
|
+
`build837I` / `build837D` write `005010X222A2` / `005010X223A3` / `005010X224A2` into ST-03 and
|
|
58
|
+
GS-08, and **a caller cannot override the value**. Two of those are not what the companion guides
|
|
59
|
+
above require, so **a trading partner that requires `005010X222A1` or `005010X223A2` will reject
|
|
60
|
+
an 837 this builder emits.** Deliberately not re-stamped: which published guide identifier a
|
|
61
|
+
partner accepts is a partner fact rather than a spec fact, and changing bytes this library
|
|
62
|
+
already emitted would break the partners it works with today. The remedy is a caller-supplied
|
|
63
|
+
override, which is a public-surface addition and its own slice.
|
|
64
|
+
|
|
19
65
|
- **🩺 BREAKING for `build277` callers: a 277 service line now REQUIRES `unitsOfService`, because
|
|
20
66
|
SVC-07 is a required element in `005010X212` and this library was not emitting it at all**
|
|
21
67
|
(`X12-277-SVC07-NOT-DECODED`). Through the release before this one `get277Status` read SVC-01
|
|
@@ -56,28 +102,34 @@ model.
|
|
|
56
102
|
on the emit side. All of it is pre-existing and reproduces at `e3cdf49`; widening the guard would
|
|
57
103
|
have turned this into that audit, which is its own item.
|
|
58
104
|
|
|
59
|
-
- **🩺 An unparseable decimal
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
`X12Decimal
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
- **🩺 An unparseable decimal reads `undefined`, and it warns.** A decimal element that is present
|
|
106
|
+
and that this library cannot decode as a decimal (`1,234.56`, `$450.00`, `N/A`) yields no value,
|
|
107
|
+
and the reader has to put something in its place. **Through `0.0.12` a slot typed `X12Decimal` got
|
|
108
|
+
`X12Decimal.ZERO`**, so a consumer that read only the model saw a `0` where the payer sent
|
|
109
|
+
something unreadable; **as of `0.0.13` no reader substitutes `X12Decimal.ZERO` for a value it did
|
|
110
|
+
not decode** - every slot that used to get one is `X12Decimal | undefined` and reads `undefined`,
|
|
111
|
+
which is a breaking type change on the read model and is the point of it. **Read that as a rule
|
|
112
|
+
about the substitution, not as a census of the model:** an optional slot already read `undefined`,
|
|
113
|
+
and some rows are dropped whole, which is a different shape with a report of its own
|
|
114
|
+
(`X12_AMOUNT_ROW_DROPPED`, the next entry). **Every one of those
|
|
115
|
+
outcomes emits `X12_UNPARSEABLE_DECIMAL` at the failing `position.elementIndex`, because the
|
|
116
|
+
warning is a property of the READ rather than of what the reader then does with it.** No list of
|
|
117
|
+
the outcomes is published here, on purpose: a first draft enumerated three and a review measured a
|
|
118
|
+
fourth, and the rule is what holds, not the census.
|
|
72
119
|
|
|
73
120
|
Three scoping facts that are easy to get wrong in the other direction:
|
|
74
|
-
- **An ABSENT element does not
|
|
75
|
-
|
|
76
|
-
|
|
121
|
+
- **An ABSENT element does not raise this code.** It reads `undefined` and
|
|
122
|
+
`X12_UNPARSEABLE_DECIMAL` is not emitted for it, so `undefined` alone does not tell you which of
|
|
123
|
+
the two happened - the warning at that `elementIndex` does, and `readElementDecimal` gives you
|
|
124
|
+
the same distinction in band. Read that as a statement about **this code**, not as "an absent
|
|
125
|
+
element is silent": where the absent element takes a whole `AMT` / `ADX` row off the model, the
|
|
126
|
+
drop is reported by `X12_AMOUNT_ROW_DROPPED` at that segment (the next entry), which is
|
|
127
|
+
raised for both routes and therefore separates neither.
|
|
128
|
+
- **🩺 An unwarned value is not thereby trustworthy in general, and this is the one inversion to
|
|
77
129
|
refuse.** The warning is a property of a decimal READ, not a property of a model slot. A slot
|
|
78
130
|
the reader never read at all cannot warn, and it still holds whatever the accumulator was seeded
|
|
79
|
-
with. **What this guarantees is narrower and exact: an unwarned
|
|
80
|
-
DECODED is
|
|
131
|
+
with. **What this guarantees is narrower and exact: an unwarned value AT AN ELEMENT A READER
|
|
132
|
+
DECODED is what the sender sent.** The known slot of the other kind, an 837 service
|
|
81
133
|
line whose `SVx` never decoded, is covered by a warning of its own (next entry) rather than by
|
|
82
134
|
this one. **No census of never-read slots is published**, on purpose: the rule is what holds.
|
|
83
135
|
- **The public helpers are silent without a sink.** `elementDecimal` / `elementDecimalOrZero` take
|
|
@@ -91,23 +143,120 @@ model.
|
|
|
91
143
|
comments. It says nothing about a decimal decoded some other way, and no exhaustive census of
|
|
92
144
|
such routes is published here, on purpose.
|
|
93
145
|
|
|
94
|
-
- **🩺 An
|
|
95
|
-
|
|
146
|
+
- **🩺 An `AMT` or `ADX` whose amount does not decode loses its WHOLE row, and says so as of
|
|
147
|
+
`0.0.13`.** These segments carry an amount plus the thing the amount is about - AMT-01's qualifier,
|
|
148
|
+
ADX-02's adjustment reason code, ADX-03 / ADX-04's reference. The readers build no row at all when
|
|
149
|
+
the amount (AMT-02, ADX-01) decodes no value, so what the sender did state goes with it: through
|
|
150
|
+
`0.0.12` `AMT*B6~` gave `claim.amounts: []` and `warnings: []`, which reads exactly like a document
|
|
151
|
+
that never carried the segment. **`X12_AMOUNT_ROW_DROPPED` now reports it**, anchored at the
|
|
152
|
+
`AMT` / `ADX` and carrying **no `elementIndex`**, because one of its two routes is an absent
|
|
153
|
+
element and an absent element has no index to name. Four surfaces raise it: the 835's `AMT`, the
|
|
154
|
+
837's `AMT`, the 834's coverage `AMT` (on that **member's** own `warnings`, exactly as the decimal
|
|
155
|
+
sink beside it is scoped) and the 820's `ADX`. On the 835 and the 837 the `AMT` attaches to the
|
|
156
|
+
open **service line** first and to the claim only when there is none, so **the row lost may be a
|
|
157
|
+
line-level one** - do not read an unchanged `claim.amounts` as evidence the warning is stale. Four
|
|
158
|
+
bounds worth stating:
|
|
159
|
+
- **It is additive, and no case moved onto it.** A present-but-undecodable amount still raises
|
|
160
|
+
`X12_UNPARSEABLE_DECIMAL` at its own `elementIndex`, now alongside this code rather than instead
|
|
161
|
+
of it, so a predicate written against that code fires on exactly the documents it fired on
|
|
162
|
+
before. Whether one accompanies this code at the same `segmentIndex` is what tells the absent
|
|
163
|
+
route from the unparseable one, since this code is raised for both and discriminates neither.
|
|
164
|
+
- **It reports a row whose AMOUNT was read and decoded no value, and nothing wider.** State it as
|
|
165
|
+
a property of the READ, not of the walker's control flow. A segment discarded before its amount
|
|
166
|
+
is read is not on this channel (the 834's `AMT` with no `HD` open, the 820's `ADX` with no
|
|
167
|
+
remittance open), and neither is one whose amount decoded and then found nothing to attach to
|
|
168
|
+
(the 835's and the 837's `AMT` before any claim). **Do not write it as "nothing open means
|
|
169
|
+
silent":** the 835 and the 837 decode first, so an `AMT` with an absent amount and no claim open
|
|
170
|
+
does raise this code.
|
|
171
|
+
- **🔴 An 820 `RMR` is not on this channel, and the reason is NOT that its row survives.**
|
|
172
|
+
`decodeRmr` drops on open-item identity (RMR-01 and RMR-02 both empty), **before** RMR-04 is
|
|
173
|
+
read. So an `RMR` that states an open item and no amount keeps its row with `amountPaid` left
|
|
174
|
+
`undefined` and there is nothing to report - but an `RMR` that states an **amount and no open
|
|
175
|
+
item** is dropped whole, taking a stated payment, its payment-action code and its amount due
|
|
176
|
+
with it. Nothing failed to decode there, so it is not this code's shape;
|
|
177
|
+
`X12_STATED_AMOUNT_DISCARDED`, below, is what reports it.
|
|
178
|
+
- **A Loop 2430 `AMT` under an open `SVD` is discarded outright, and this code is not what says
|
|
179
|
+
so.** With a claim and a line open, the 837's adjudication skip drops an `AMT` that decoded
|
|
180
|
+
perfectly well. Through `0.0.12` that was silent while one that decoded nothing raised this
|
|
181
|
+
code, so the report was present exactly where less was lost;
|
|
182
|
+
`X12_STATED_AMOUNT_DISCARDED`, below, closed that.
|
|
183
|
+
|
|
184
|
+
- **🩺 A row the sender DID state can still be discarded, and says so as of `0.0.13`.** The code
|
|
185
|
+
above is about an amount this library could not read. This is the opposite case: **the reader
|
|
186
|
+
discarded the row for a reason that is not about the amount at all**, so the amount was never the
|
|
187
|
+
problem and, on one of the two routes, was never even looked at. The bytes stay verbatim on
|
|
188
|
+
`tx.segments[…].raw`; decode them yourself. Through `0.0.12` both routes were silent on every
|
|
189
|
+
channel. **`X12_STATED_AMOUNT_DISCARDED` reports them**, anchored at
|
|
190
|
+
the segment and carrying **no `elementIndex`**. The two routes, enumerated:
|
|
191
|
+
- An **820 `RMR`** under an open remittance loop whose RMR-01 and RMR-02 are **both empty** while
|
|
192
|
+
RMR-04 or RMR-05 is populated. `decodeRmr` refuses the open item on identity before either
|
|
193
|
+
amount element is read, so a stated payment, a stated amount due and RMR-03's payment action
|
|
194
|
+
code leave the model together. A bare `RMR~` states nothing and stays silent.
|
|
195
|
+
- An **837 `AMT`** arriving while a Loop 2430 line adjudication (`SVD`) is open, whose AMT-02
|
|
196
|
+
decoded. The v1 adjudication model carries no amount row, and attaching one to this
|
|
197
|
+
submission's own service line would put another payer's figure on it, so the row is skipped.
|
|
198
|
+
`AMT*EAF` (Remaining Patient Liability) on a Loop 2430 is exactly the shape being lost.
|
|
199
|
+
|
|
200
|
+
Three bounds worth stating:
|
|
201
|
+
- **It is additive, and nothing moved onto it.** `X12_AMOUNT_ROW_DROPPED` and
|
|
202
|
+
`X12_UNPARSEABLE_DECIMAL` fire on exactly the documents they fired on before, pinned by
|
|
203
|
+
committed tests.
|
|
204
|
+
- **The two amount-row codes are disjoint and can never name the same segment**, because this one
|
|
205
|
+
requires an amount element the sender populated and the other requires one that decoded no
|
|
206
|
+
value. The code you get is the discriminant.
|
|
207
|
+
- **It reports a segment that arrived while the loop that would carry its row was open.** An
|
|
208
|
+
`AMT` or `ADX` reaching a reader with **no such loop open** is a different loss and is **still
|
|
209
|
+
silent**: the 834's `AMT` with no `HD` open, the 820's `ADX` with no remittance open, and the
|
|
210
|
+
835's and the 837's `AMT` before any claim or service line is open. **🔴 Read "still silent"
|
|
211
|
+
literally and no wider: at those last two sites the inversion above SURVIVES.** With no claim
|
|
212
|
+
open an 835 `AMT*B6~` raises `X12_AMOUNT_ROW_DROPPED` while `AMT*B6*500.00~` raises nothing, so
|
|
213
|
+
the channel still reports the smaller loss there. `PRE-EXISTING` and unchanged by this release. And on the `RMR` route this
|
|
214
|
+
code says nothing about whether the amount would have decoded, because the row is refused before
|
|
215
|
+
the decode is attempted, so **no `X12_UNPARSEABLE_DECIMAL` accompanies it even where the bytes
|
|
216
|
+
are unreadable.** It is raised on `RMR****1,234.56~` exactly as on `RMR****150.00*150.00~`, so
|
|
217
|
+
**never read an unaccompanied instance as evidence the bytes are postable**; only the `AMT`
|
|
218
|
+
route guarantees a decodable amount, because there AMT-02 decoded before the row was skipped.
|
|
219
|
+
|
|
220
|
+
- **🩺 An 835 balance invariant with an undecoded term is reported as UNEVALUABLE, not as a
|
|
221
|
+
mismatch.** The three TR3 005010X221A1 §1.10.2 equations (line, claim, top-of-remit) read amounts
|
|
222
|
+
that are all `X12Decimal | undefined` as of `0.0.13`. Where any term of an equation is
|
|
223
|
+
`undefined`, the equation is not run and `X12_835_BALANCE_NOT_EVALUABLE` is emitted instead of
|
|
224
|
+
`X12_835_REMIT_BALANCE_MISMATCH`, because nothing was measured out of balance: substituting `0`
|
|
225
|
+
for the missing term would be this library asserting a total nobody sent. **Through `0.0.12` that
|
|
226
|
+
substitution is exactly what happened**, so an 835 with an absent `CLP-03` reported a mismatch
|
|
227
|
+
between the payer's own amounts and an invented zero. Two bounds worth stating:
|
|
228
|
+
- **An EMPTY list is not an absent term.** A claim carrying no `CAS` really did state no
|
|
229
|
+
adjustments, and that sums to `X12Decimal.ZERO`. Only a term this library decoded no value from
|
|
230
|
+
stops the equation.
|
|
231
|
+
- **`build835` cannot reach it from TypeScript.** Every balance term on `Build835Spec` is a
|
|
232
|
+
required `X12Decimal`. A JS caller passing `undefined` gets `X12_835_BUILD_INVALID_SPEC` (an
|
|
233
|
+
untyped `TypeError` through `0.0.12`), deliberately NOT the build-side balance-mismatch code.
|
|
234
|
+
|
|
235
|
+
- **The public `elementDecimalOrZero` helper still substitutes `X12Decimal.ZERO`, and no reader in
|
|
236
|
+
this library calls it any more.** Its documented behaviour is unchanged and it stays exported, so
|
|
237
|
+
a consumer walking segments itself can still opt into the substitution - knowingly, rather than by
|
|
238
|
+
inheriting a convention the `get*` readers no longer follow. `elementDecimal` and
|
|
239
|
+
`readElementDecimal` are the honest routes.
|
|
240
|
+
|
|
241
|
+
- **🩺 An 837 service line whose `SVx` never decoded ships with `charge` and `units` `undefined`,
|
|
242
|
+
and it warns.** `get837Claims` resolves ONE variant for the submission, from the
|
|
96
243
|
caller's `type` option, else ST-03's implementation-convention reference, else the first `SVx`
|
|
97
244
|
segment present. A Loop 2400 line is then decoded only by the `SV1` / `SV2` / `SV3` that matches
|
|
98
245
|
that variant. When none arrives, because the line carries an `SVx` for a different variant (an
|
|
99
246
|
ST-03 of `005010X222A2` on a file whose lines are `SV2`, or a `{ type: "P" }` over the same) or
|
|
100
247
|
because it carries no `SVx` at all, **nothing on the service segment is read**: the line's
|
|
101
|
-
`charge` and `units` hold the accumulator's seeded `
|
|
248
|
+
`charge` and `units` hold the accumulator's seeded `undefined`, and its procedure code,
|
|
102
249
|
modifiers, unit of measure and place of service are equally undecoded. **That line now emits
|
|
103
250
|
`X12_837_SERVICE_LINE_NOT_DECODED`, anchored at the `LX` that opened it.** Read the values off
|
|
104
251
|
`tx.segments[…].raw` and decide which of the two disagreeing signals the sender meant.
|
|
105
252
|
- **Ignoring the foreign `SVx` is deliberate and is not the limitation.** `SV1-02` and `SV2-03`
|
|
106
253
|
are both the line charge, so decoding an `SV2` into a Professional line would mis-read money.
|
|
107
254
|
Refusing to read is the safe half; doing it silently was the defect.
|
|
108
|
-
-
|
|
109
|
-
`
|
|
110
|
-
`
|
|
255
|
+
- **`undefined` does not say WHY, which is what the warning is for.** `charge` and `units` are
|
|
256
|
+
`X12Decimal | undefined` as of `0.0.13` (`X12Decimal` reading a fabricated `0` through
|
|
257
|
+
`0.0.12`), so a consumer that never reads `.warnings` at least sees that nothing was decoded -
|
|
258
|
+
but it cannot tell this case, where no `SVx` was read at all, from an `SVx` that WAS read and
|
|
259
|
+
carried no charge element. Only `X12_837_SERVICE_LINE_NOT_DECODED` separates them.
|
|
111
260
|
- **The line is still retained, and so are the bytes.** Nothing is dropped: the service line is
|
|
112
261
|
on the claim, and every segment, decoded or not, stays verbatim on `tx.segments`.
|
|
113
262
|
- **Neither cause is attributed to the sender.** A `type` option that disagrees with a perfectly
|
|
@@ -214,20 +363,102 @@ model.
|
|
|
214
363
|
- **The segment is NOT decoded into any line.** `SV1-02` and `SV2-03` are both the line charge, so
|
|
215
364
|
reading a service segment into a line the walker never opened mis-reads money. Refusing to read
|
|
216
365
|
is the safe half; doing it silently was the defect. **This does not change the `SV1-02` case
|
|
217
|
-
above:** an absent `SV1-02` on a line that DID open
|
|
218
|
-
|
|
366
|
+
above:** an absent `SV1-02` on a line that DID open reads `undefined` as of `0.0.13`, a
|
|
367
|
+
confident `0` through `0.0.12`, and this code has nothing to do with either.
|
|
219
368
|
- **🩺 IT SAYS NOTHING ABOUT THE VARIANT, AND A FIRST DRAFT OF THIS BOUND CLAIMED IT DID.** Variant
|
|
220
369
|
resolution runs before the walk. A caller-supplied `type` option wins first; absent one, and
|
|
221
|
-
where `ST-03` names
|
|
370
|
+
where `ST-03` names no implementation convention this reader recognises, it **falls back to the
|
|
222
371
|
first `SVx` segment id anywhere in the transaction body - orphans included**. So a stray `SV2`
|
|
223
|
-
under an `ST-03`
|
|
224
|
-
conformant `SV1` line in it then reads `charge` `
|
|
372
|
+
under an unrecognised `ST-03` re-types the whole submission as Institutional, and every
|
|
373
|
+
conformant `SV1` line in it then reads `charge` `undefined`, `units` `undefined` (both a
|
|
374
|
+
fabricated `0` through `0.0.12`) and an `undefined` procedure
|
|
225
375
|
code - `undefined`, not `""`, which on such a line is the `revenueCode`. Passing
|
|
226
376
|
`{ type: "P" }` reads the same document correctly. Measured, both trees: `PRE-EXISTING`,
|
|
227
377
|
identical at `0.0.10`, **not** introduced or changed by this code, and **not** narrowed here - excluding
|
|
228
378
|
orphans from the fallback would change how existing documents decode and is its own slice. It
|
|
229
379
|
is warned rather than silent (`X12_837_SERVICE_LINE_NOT_DECODED` at each `LX`), and
|
|
230
|
-
`submission.variant` is the field that tells you.
|
|
380
|
+
`submission.variant` is the field that tells you. **The resolution itself is now reported too
|
|
381
|
+
wherever it was contested**, which is the entry below; the fallback is still not narrowed.
|
|
382
|
+
|
|
383
|
+
- **🩺 An 837 variant the `SVx` fallback resolved in a body that names more than one variant raises
|
|
384
|
+
`X12_837_AMBIGUOUS_VARIANT`, anchored at the `ST`.** The fallback is unchanged and is deliberately
|
|
385
|
+
NOT narrowed: it still takes the **first** `SV1` / `SV2` / `SV3` in the body, orphans included, so on
|
|
386
|
+
every document that reaches it the variant resolved is byte-for-byte what it was through `0.0.13`.
|
|
387
|
+
**WHICH documents reach it changed in this release** - see the ST-03 entry at the top of this list. What this code
|
|
388
|
+
adds is that the resolution said so. Through `0.0.13` a stray `SV2` re-typed a whole Professional
|
|
389
|
+
submission Institutional and **only the line-level consequences were on any channel** - a consumer
|
|
390
|
+
routing on `submission.variant` saw a confident `"I"` with nothing to contradict it. Four bounds,
|
|
391
|
+
each a committed test:
|
|
392
|
+
- **It reports the RESOLUTION, never the document.** A caller-supplied `type` wins ahead of the
|
|
393
|
+
fallback, and so does an `ST-03` this reader turns into a variant; in
|
|
394
|
+
either case no guess was made and this code is not raised **however mixed the body is**. Never
|
|
395
|
+
restate it as "the file carries more than one kind of service segment".
|
|
396
|
+
- **🩺 Which service segment is the stray one is NOT decided, and nothing here should ever start
|
|
397
|
+
deciding it.** This reader cannot tell a stray service segment from a conformant one, and the
|
|
398
|
+
fallback takes the first whether or not a Loop 2400 was open at it, so an orphan decides the
|
|
399
|
+
variant like any other. Reporting the conflict is honest; picking a winner would be inventing.
|
|
400
|
+
- **It is ADDITIVE and nothing moved onto it.** `X12_837_SERVICE_LINE_NOT_DECODED`,
|
|
401
|
+
`X12_837_SERVICE_SEGMENT_WITHOUT_LX` and `X12_837_SERVICE_LINE_DROPPED` fire on exactly the
|
|
402
|
+
documents they fired on before **this code was added**, in the same positions, and no predicate
|
|
403
|
+
changes meaning because of this code. **That is a claim about this code and NOT about the
|
|
404
|
+
release:** the ST-03 entry at the top of this list changed which documents reach the fallback at
|
|
405
|
+
all, and a line whose `SVx` kind disagrees with a now-recognised `ST-03` stops decoding and
|
|
406
|
+
starts raising `X12_837_SERVICE_LINE_NOT_DECODED`. Read that entry beside this bound.
|
|
407
|
+
- **It can never travel with `X12_837_UNKNOWN_VARIANT`**, which is the other outcome of the same
|
|
408
|
+
resolution: a body with conflicting service segments has, by construction, at least one to fall
|
|
409
|
+
back on. It fires **once per transaction**, because there is one resolution per transaction.
|
|
410
|
+
- **What it does NOT close:** narrowing the fallback, which is deliberately left alone. It no
|
|
411
|
+
longer leaves a second `SVx` inside an already-open Loop 2400 unreported: that was its own
|
|
412
|
+
slice, and it is the entry below. **Read the earlier wording as withdrawn** - it said such a
|
|
413
|
+
segment is "read into nothing", which was true only of one whose kind does not match the
|
|
414
|
+
resolved variant; a DUPLICATE of the matching kind is read into the line and REPLACES what the
|
|
415
|
+
first one wrote.
|
|
416
|
+
|
|
417
|
+
- **🩺 A second `SV1` / `SV2` / `SV3` inside an ALREADY-OPEN Loop 2400 raises
|
|
418
|
+
`X12_837_SERVICE_SEGMENT_REPEATED`, anchored at the repeated service segment itself.** A service
|
|
419
|
+
line carries **one** service segment's worth of slots, and every decoder writes **all** of the
|
|
420
|
+
slots its kind writes, so the model holds what the **last** service segment matching the
|
|
421
|
+
submission's resolved variant wrote and nothing of any earlier one. **Through `0.0.13` that
|
|
422
|
+
happened in complete silence, and it is money and a procedure code:** under an `ST-03` of
|
|
423
|
+
`005010X222A2`, `SV1*HC:99213*8500*UN*4***1~` followed by `SV1*HC:99999*12*UN*1***1~` inside one
|
|
424
|
+
`LX` left ONE line reading `charge` `12` and `procedureCode` `99999`, with `warnings: []`. `8500`
|
|
425
|
+
became `12`, CPT `99213` became `99999`, and nothing was raised on any channel. Six bounds, each a
|
|
426
|
+
committed test:
|
|
427
|
+
- **🛑 The decode is NOT narrowed, and that is deliberate.** Last-wins is unchanged, element for
|
|
428
|
+
element, so which values a document decodes to are byte-for-byte what they were at `0.0.13`.
|
|
429
|
+
This reader cannot tell a stray service segment from a conformant one, so choosing the first
|
|
430
|
+
instead would be inventing; and changing which occurrence wins changes how **already-published
|
|
431
|
+
documents decode**, the same call made about the variant fallback in the entry above. **This
|
|
432
|
+
closes only the silence.**
|
|
433
|
+
- **🩺 The worst corner is a repeat whose own charge element is ABSENT.** It writes `undefined`
|
|
434
|
+
over the amount the first one stated, and `X12_837_SERVICE_LINE_NOT_DECODED` does **not** fire
|
|
435
|
+
on that line, because a service segment _did_ decode. This code is then the only thing on the
|
|
436
|
+
channel that says why the charge is empty.
|
|
437
|
+
- **It fires on a repeat of ANY kind, decoded or not.** A service segment whose kind does not
|
|
438
|
+
match the resolved variant is read into nothing and overwrites nothing - the loss there is that
|
|
439
|
+
what it carries reaches no part of the typed model - and it is reported the same way, whether it
|
|
440
|
+
arrives before or after the matching one. That case was silent at the segment through `0.0.13`
|
|
441
|
+
too, and the entry above recorded it as a deferred residual.
|
|
442
|
+
- **Once per repeat, and the count is scoped to the LINE, never latched.** Three service segments
|
|
443
|
+
in one Loop 2400 are two warnings at two positions; a first service segment under a later `LX`,
|
|
444
|
+
or in a later claim, is a first and never a repeat. A latching flag would report a conformant
|
|
445
|
+
second line.
|
|
446
|
+
- **It can never name the same segment as `X12_837_SERVICE_SEGMENT_WITHOUT_LX`**, which requires
|
|
447
|
+
that NO Loop 2400 be open where this one requires that one is. It can travel with
|
|
448
|
+
`X12_837_SERVICE_LINE_NOT_DECODED` (at the `LX`), with `X12_837_AMBIGUOUS_VARIANT` (at the
|
|
449
|
+
`ST`), and with `X12_UNPARSEABLE_DECIMAL` raised by the repeat's own amount bytes. **That last
|
|
450
|
+
pair sits at the SAME `position.segmentIndex`, separated only by the decimal code's own
|
|
451
|
+
`position.elementIndex`** - measured, and pinned by a committed test. Never read a co-occurring
|
|
452
|
+
pair here as two segments.
|
|
453
|
+
- **It is ADDITIVE and nothing moved onto it.** Every code this reader raised on a document of
|
|
454
|
+
this shape before **this code was added**, it still raises, at the same position, and no
|
|
455
|
+
predicate changes meaning because of this code. Read that as invariance and **not** as a list of
|
|
456
|
+
what else you will see, and **not** as a claim about the release: the ST-03 entry at the top of
|
|
457
|
+
this list changed which documents reach the variant fallback.
|
|
458
|
+
**The package's own documentation was a consumer that needed updating**: the cookbook's
|
|
459
|
+
"gate before you post a line amount" recipe named four codes, none of which fires on the
|
|
460
|
+
overwrite document, so it was blind to it - it now names this one too, and a committed test
|
|
461
|
+
pins that the four-code gate misses what the five-code gate catches.
|
|
231
462
|
|
|
232
463
|
- **🩺 An `N3` / `N4` / `PER` / `REF` discarded after a stray `LX` raises
|
|
233
464
|
`X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX`, anchored at the discarded segment itself.** This is
|
|
@@ -265,6 +496,58 @@ model.
|
|
|
265
496
|
following a **stray** `LX` belongs to is not derivable from the TR3s in either direction, so the
|
|
266
497
|
reader refuses to attribute it rather than guessing.
|
|
267
498
|
|
|
499
|
+
- **🩺 An 837 Loop 2000A that names the pay-to address more than once has only ONE slot on the model
|
|
500
|
+
to put it in, and the model carries one of the addresses rather than both.** The TR3s allow Loop
|
|
501
|
+
2010AB at most once per Loop 2000A, so a repeated `NM1*87` is a non-conformant document. Each one
|
|
502
|
+
raises `X12_837_PAY_TO_ADDRESS_REPEATED`, anchored at the repeated `NM1*87`, so the choice below is
|
|
503
|
+
never made in silence.
|
|
504
|
+
|
|
505
|
+
**🩺 Through `0.0.12` the two addresses were FUSED, and that is what changed.** `payToAddress` is a
|
|
506
|
+
bare accumulator with no entity object to own it, so unlike every other party there was nothing to
|
|
507
|
+
be replaced at the repeat: `withLines` appended and `mergeAddress` fell back, and two `NM1*87`s
|
|
508
|
+
each carrying an `N3` and an `N4` read back a street line from **each** address plus a
|
|
509
|
+
`countryCode` off the **first** `N4`, on an address whose own `N4` names no country, with
|
|
510
|
+
`warnings: []`. Re-emitted through `build837P` that was one Loop 2010AB naming a payment
|
|
511
|
+
destination no sender stated. **If you read a repeated `NM1*87` on `0.0.12` or earlier, treat that
|
|
512
|
+
claim's `payToAddress` as unreliable rather than as either sender's address.**
|
|
513
|
+
|
|
514
|
+
**The rule now applied, and it is stated in the emit side's terms because the emit side reads this
|
|
515
|
+
slot:** occurrences are never merged; the **last occurrence that states an address of its own**
|
|
516
|
+
takes the slot; and an occurrence that states none - one with no `N3` or `N4` at all, or only a
|
|
517
|
+
valueless `N3` / `N4` whose elements are empty - **does not blank one that did**. "States an
|
|
518
|
+
address" means exactly what `emitAddress` would write a segment for, and reader and writer share
|
|
519
|
+
one predicate so they cannot disagree. That last clause is not a nicety: `build837P/I/D` gates Loop
|
|
520
|
+
2010AB on `payToAddress !== undefined`, so a slot cleared or blanked by a repeat that stated
|
|
521
|
+
nothing would re-emit as **no pay-to loop**, or as a **bare `NM1*87`** carrying neither `N3` nor
|
|
522
|
+
`N4`. Both say something about where a payment goes that the sender did not.
|
|
523
|
+
|
|
524
|
+
**🩺 The cost, which is real and is not argued away: a repeat that states only PART of an address
|
|
525
|
+
puts only that part on the model, and re-emits only that part.** A second `NM1*87` followed by an
|
|
526
|
+
`N4` and no `N3` reads back that `N4` with `lines: []` and re-emits a Loop 2010AB with no `N3`,
|
|
527
|
+
where `0.0.12` emitted a complete-looking address assembled from two. Keeping the earlier street
|
|
528
|
+
lines is the fusion itself, so they are not kept. **Which occurrence the sender meant is not
|
|
529
|
+
derivable from the TR3s**, and the losing occurrence's address is **not on the model in any form** -
|
|
530
|
+
`tx.segments` is where its bytes are, and remains the only complete account of the document.
|
|
531
|
+
|
|
532
|
+
**Two bounds, both measured.** This is scoped to the pay-to route, which lives in Loop 2000A: an
|
|
533
|
+
`NM1*87` arriving while a `CLM` is open never reaches that route, and is neither resolved nor
|
|
534
|
+
warned by this code. **Where it lands instead depends on whether a service line is open, and this
|
|
535
|
+
document does not state a single destination for it**, because two were measured: with a Loop 2400
|
|
536
|
+
open it joins that line's `serviceLine.providers` (a line-level provider is TR3 Loop 2420), and
|
|
537
|
+
with a claim but no line open it joins `claim.providers` (Loop 2310). Both are pre-existing and
|
|
538
|
+
identical at `0.0.12`. And a document with **at most one** `NM1*87` per Loop 2000A is unaffected in
|
|
539
|
+
every respect, warning channel included - including the pre-existing case of a lone `NM1*87` with a
|
|
540
|
+
valueless `N3`, which still reads an address with no elements and still re-emits a bare `NM1*87`.
|
|
541
|
+
|
|
542
|
+
**🩺 One thing the cost above shares with the shape that refuted remedy 2, stated so the two are
|
|
543
|
+
not read as different in kind.** In Loop 2010AB the TR3s make `NM1`, `N3` **and** `N4` all Required,
|
|
544
|
+
so a re-emitted loop carrying only the winning occurrence's `N4` is short a Required `N3`, exactly
|
|
545
|
+
as a bare `NM1*87` is short both. The difference is not conformance, it is provenance: every
|
|
546
|
+
element that IS emitted was stated by the occurrence the model kept, where the fused address
|
|
547
|
+
contained elements no single occurrence stated. Emitting a Loop 2010AB short a Required segment is
|
|
548
|
+
a pre-existing property of `emitAddress`, reachable at `0.0.12` from a lone `NM1*87` with an
|
|
549
|
+
`N4` and no `N3`, and is not introduced here.
|
|
550
|
+
|
|
268
551
|
- **🩺 Through `0.0.9`, a lookup keyed by document bytes could be defeated by a key inherited from
|
|
269
552
|
`Object.prototype`, and the affected code paths reported nothing.** The bundled code lists, the
|
|
270
553
|
837's variant resolution, and the 837's HL parent-level map were built as plain object literals,
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ const ix = parseX12(rawEdi); // never throws except on 4 structural fatals
|
|
|
34
34
|
const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "835");
|
|
35
35
|
const remit = tx ? get835(ix.delimiters, tx) : undefined;
|
|
36
36
|
|
|
37
|
-
remit?.payment.totalActualPayment
|
|
37
|
+
remit?.payment.totalActualPayment?.toString(); // "450.00": BigInt-exact, never a float
|
|
38
38
|
remit?.claims[0]?.patientControlNumber; // "PT-ACCT-001": your account number, echoed back
|
|
39
39
|
remit?.claims[0]?.serviceLines[0]?.adjustments[0]?.reasonCode; // "1": CARC (why it was adjusted)
|
|
40
40
|
```
|
|
@@ -45,7 +45,7 @@ That's the pitch: no schema upload, no spec knowledge. The parser accepts vendor
|
|
|
45
45
|
|
|
46
46
|
- **Typed read + emit, per transaction**: 271, 277/277CA, 278, 820, 834, 835, 837P/I/D, plus 999/TA1 acknowledgments. Per-transaction helpers (`get835`, `get837Claims`, `get271Eligibility`, …) and a matching domain builder for each (`build835`, `build837P/I/D`, `build271`, …). The 270 and 276 inquiries parse as generic segments; they have no typed model yet.
|
|
47
47
|
- **Postel's Law**: a lenient parser (deviations → warnings with a stable code + positional context) and a conservative serializer. `serializeX12` is byte-faithful **for the segments on the model** by default: each comes back verbatim, including element padding, composites, and `?`-release escapes. **`serialize(parse(s)) === s` is not guaranteed in general.** Six constructs are known not to survive: line breaks between segments (any run of CR / LF between segments is absorbed at parse, so a pretty-printed or double-spaced file emits compact), a doubled terminator outside a transaction, a missing final terminator (the emit supplies one), post-IEA `trailingBytes` (re-joined, not verbatim), a TA1 that followed a functional group (emitted right after the ISA, so it is **reordered**, though nothing is lost), and a segment whose first element is empty outside a transaction (skipped entirely, with no warning at all). A **segment outside a transaction is not on that list**: it is reported as `X12_UNEXPECTED_SEGMENT`, kept on the model at `ix.orphanSegments` with a structural `anchor`, and re-emitted at that anchor, so the segment, its value and its warning all survive the round trip - placement is by the anchor and never by `segmentIndex`, which indexes the input stream the emit does not follow. The last five break the round trip on inputs with no line breaks at all, so do not treat "no line breaks" as sufficient; five of the six are silent, so a clean warnings list does not mean byte-exact either. Measured across this repo's 56 fixtures: every emit is a fixed point and re-parses to an identical model with an identical warning stream, the 14 with no line breaks return byte-identical, and the other 42 differ by line breaks and nothing else. See [Line endings between segments](./docs-content/spec-notes-envelope.md). `{ specClean: true }` reconciles the envelope, and `{ specClean: true, recomputeCounts: true }` also emits the corrected counts (`recomputeCounts` does nothing on its own). A mismatch is always warned, never silently corrected. Only **4 structural failures** are ever fatal.
|
|
48
|
-
- **Money is exact**: every monetary/percent/quantity field decodes as `X12Decimal` (string-backed, BigInt arithmetic)
|
|
48
|
+
- **Money is exact, and never invented**: every monetary/percent/quantity field decodes as `X12Decimal` (string-backed, BigInt arithmetic), and the library **never `parseFloat`s** an EDI amount. **No slot holds an amount this library did not decode.** Where a reader used to substitute `X12Decimal.ZERO` the slot is now `X12Decimal | undefined` and reads `undefined`, so "the sender stated zero" and "the sender stated nothing" are different readings; through `0.0.12` they were the same `0`. That is a rule about the substitution, not a census of the model: some rows are dropped whole instead, which is a different and pre-existing shape, and no total is published here. Read [KNOWN-LIMITATIONS.md](./KNOWN-LIMITATIONS.md).
|
|
49
49
|
- **Safety-critical fidelity**: TRN reassociation traces, 835 balance invariants, 837 HL hierarchy integrity, 834 maintenance types, and 278 certification actions are preserved **verbatim** and never inferred; ambiguity yields a warning or a typed refusal, never a confident wrong answer.
|
|
50
50
|
- **PHI-disciplined**: synthetic-only fixtures, a PHI commit-gate, and a warning `message` that is a **lookup into a frozen registry**, not something built from your document. No warning factory in the library takes a value parameter, so no element can reach a diagnostic: the code and `position` say what and where, and the bytes stay on the model. `ALL_WARNING_MESSAGES` is exported so you can assert it. **Builder refusals are a different, and deliberately weaker, surface:** twenty-four refusal sites across ten builder modules name a value you passed in, so you can see which control number, count or code was refused. **What they will never name is a `claimId`, a member id, a member name, a trace or a diagnosis code** - a guarantee that held for the refusal templates and, until the release after `0.0.10`, did not hold for the shared type guards underneath them: a JSON-driven caller who sent a number where the types say string got `a number ("900412345678")` back, bounded but not redacted, from a guard standing on every element of every builder. Those guards report the type now and never the value. Only the segment-join guard names the slot as well; the `esc` and `X12Decimal` guards name the builder, which is a diagnostic cost stated rather than hidden. The array guard's primitive arm is redacted with them, but it still reports the `length` and the class tag of a forged array-like, bounded, because those describe the shape you forged rather than the contents of a document element. Each goes through `renderCallerValue`, capping the rendered **fragment** at `BUILD_REFUSAL_VALUE_MAX_RENDERED` (90 characters); all three names are exported so you can assert the ceiling. The whole `message` is that plus the site's own fixed text, so it is bounded by a constant but a larger one: a 120,000-character control number gave a 120,066-character `X12BuildError.message` before this and gives 150 now. That is robustness, not redaction: the value is one you supplied and bounding it hides nothing from you. It is also not escaped, and on the **ack** path it is not always strictly your own, since `build999`'s AK2-02 and `buildTA1` echo an inbound document's control numbers by design. Log `err.code`, not `err.message`, from a builder. **`defineProfile()` is bounded on the same terms since `0.0.6`** (twelve refusal sites, twenty-three caller values; the worst message measured **360,181 characters** before and **431** now, both at the `fixture` refusal, which names three caller values; the ceiling for that site is 443 and the suite asserts every site under 500; `X12ProfileError.profileName` is deliberately left unbounded so it still matches the name you passed). **And a forged non-array in a builder spec now refuses instead of hanging:** every indexed loop takes its bound from a checked array, so `{ length: "9".repeat(120000) }` draws a typed refusal rather than coercing to `Infinity` and looping forever. A few `for…of` reads still throw an untyped `TypeError` instead; both are in [KNOWN-LIMITATIONS.md](./KNOWN-LIMITATIONS.md). **The one deliberate exception:** `X12ParseError.snippet` on the four Tier-3 structural fatals is a bounded (≤ 64 character) copy of the start of the input, so on real traffic it can carry PHI. The library does not redact it. Redact at your call site, or log `err.code` and `err.position` instead. See [Keeping PHI out of logs](./docs-content/troubleshooting.md).
|
|
51
51
|
|