nx_medbill_recon.nx source
↩ module page · 275 lines · 10395 B
1// nx_medbill_recon.nx -- M2 of the MEDICAL BILLING ladder (/compare/medbilling): parse an ITEMIZED BILL
2// or EOB text document into structured claim lines, then RECONCILE bill-vs-EOB with exact no-float cents.
3// A claim line = a text line carrying ALL THREE of: a date, a 5-char procedure code (CPT/HCPCS shape,
4// >=4 digits), and at least one $-money token -- header/footer prose never qualifies (an account number
5// alone is not a claim line). Money REQUIRES the $ prefix (a bare year is never money). EOB lines carry
6// billed/allowed/patient columns (first/second/last money); a bill line's charge = its LAST money.
7// RECON is a CONSUMING matcher (code+date, each EOB line matches at most once -- so a duplicate bill line
8// has nothing left to match = NOT-ON-EOB) with flags: 1=NOT_ON_EOB (amount=charge), 2=BILLED_MISMATCH
9// (|bill - EOB billed|), 3=ABOVE_ALLOWED (charge - allowed). It reports; it never fabricates a line and
10// never auto-acts (findings feed the fail-safe R8 dispute letter). license_tier: ORIGINAL
11import "nx_syscalls.nx"
12const K_MAGIC_1900: i64 = 1900
13const K_MAGIC_10000: i64 = 10000
14
15// ---- ctx layout (i64 slots): 0=n 1=codes-arena(16B/entry) 2=dates 3=units 4=m0 5=m1 6=m2 7=nmoney 8=cap
16func mbr_ctx_new(cap: i64) -> *i64 {
17 let ctx: *i64 = sys_mmap(8 * 16) as *i64
18 ctx[0] = 0
19 let arena: *u8 = sys_mmap(16 * cap)
20 ctx[1] = arena as i64
21 ctx[2] = sys_mmap(8 * cap)
22 ctx[3] = sys_mmap(8 * cap)
23 ctx[4] = sys_mmap(8 * cap)
24 ctx[5] = sys_mmap(8 * cap)
25 ctx[6] = sys_mmap(8 * cap)
26 ctx[7] = sys_mmap(8 * cap)
27 ctx[8] = cap
28 return ctx
29}
30func mbr_code_at(ctx: *i64, i: i64) -> *u8 { return ((ctx[1] as i64) + 16 * i) as *u8 }
31func mbr_geti(ctx: *i64, slot: i64, i: i64) -> i64 { let p: *i64 = ctx[slot] as *i64; return p[i] }
32
33func mbr_is_digit(c: u8) -> i64 { if (c as i64) >= 48 { if (c as i64) <= 57 { return 1 } } return 0 }
34func mbr_is_upper(c: u8) -> i64 { if (c as i64) >= 65 { if (c as i64) <= 90 { return 1 } } return 0 }
35
36// money token: $D[,DDD]*[.CC] -> cents, or -1
37func mbr_tok_money(t: *u8, ts: i64, te: i64) -> i64 {
38 if (t[ts] as i64) != 36 { return 0 - 1 }
39 var v: i64 = 0
40 var seen: i64 = 0
41 var i: i64 = ts + 1
42 while i < te {
43 let c: i64 = t[i] as i64
44 if c == 44 { i = i + 1 } else {
45 if c == 46 {
46 if te - i != 3 { return 0 - 1 }
47 if mbr_is_digit(t[i + 1]) == 0 { return 0 - 1 }
48 if mbr_is_digit(t[i + 2]) == 0 { return 0 - 1 }
49 let cc: i64 = ((t[i + 1] as i64) - 48) * 10 + ((t[i + 2] as i64) - 48)
50 if seen == 0 { return 0 - 1 }
51 return v * 100 + cc
52 }
53 if mbr_is_digit(t[i]) == 0 { return 0 - 1 }
54 v = v * 10 + (c - 48)
55 seen = 1
56 i = i + 1
57 }
58 }
59 if seen == 0 { return 0 - 1 }
60 return v * 100
61}
62
63// date token: M/D/YYYY or MM/DD/YYYY or YYYY-MM-DD -> yyyymmdd, or -1
64func mbr_tok_date(t: *u8, ts: i64, te: i64) -> i64 {
65 var p1: i64 = 0 - 1
66 var p2: i64 = 0 - 1
67 var sep: i64 = 0
68 var i: i64 = ts
69 while i < te {
70 let c: i64 = t[i] as i64
71 if c == 47 { if p1 < 0 { p1 = i } else { if p2 < 0 { p2 = i } } sep = 47 }
72 if c == 45 { if p1 < 0 { p1 = i } else { if p2 < 0 { p2 = i } } sep = 45 }
73 i = i + 1
74 }
75 if p1 < 0 { return 0 - 1 }
76 if p2 < 0 { return 0 - 1 }
77 var a: i64 = 0
78 i = ts
79 while i < p1 { if mbr_is_digit(t[i]) == 0 { return 0 - 1 } a = a * 10 + ((t[i] as i64) - 48); i = i + 1 }
80 var b: i64 = 0
81 i = p1 + 1
82 while i < p2 { if mbr_is_digit(t[i]) == 0 { return 0 - 1 } b = b * 10 + ((t[i] as i64) - 48); i = i + 1 }
83 var c3: i64 = 0
84 i = p2 + 1
85 while i < te { if mbr_is_digit(t[i]) == 0 { return 0 - 1 } c3 = c3 * 10 + ((t[i] as i64) - 48); i = i + 1 }
86 if sep == 47 {
87 if a < 1 { return 0 - 1 }
88 if a > 12 { return 0 - 1 }
89 if b < 1 { return 0 - 1 }
90 if b > 31 { return 0 - 1 }
91 if c3 < K_MAGIC_1900 { return 0 - 1 }
92 return c3 * K_MAGIC_10000 + a * 100 + b
93 }
94 if a < K_MAGIC_1900 { return 0 - 1 }
95 if b < 1 { return 0 - 1 }
96 if b > 12 { return 0 - 1 }
97 if c3 < 1 { return 0 - 1 }
98 if c3 > 31 { return 0 - 1 }
99 return a * K_MAGIC_10000 + b * 100 + c3
100}
101
102// units token: x8 / X8 -> count, or -1
103func mbr_tok_units(t: *u8, ts: i64, te: i64) -> i64 {
104 let c0: i64 = t[ts] as i64
105 if c0 != 120 { if c0 != 88 { return 0 - 1 } }
106 if te - ts < 2 { return 0 - 1 }
107 var v: i64 = 0
108 var i: i64 = ts + 1
109 while i < te { if mbr_is_digit(t[i]) == 0 { return 0 - 1 } v = v * 10 + ((t[i] as i64) - 48); i = i + 1 }
110 if v < 1 { return 0 - 1 }
111 return v
112}
113
114// procedure-code shape: exactly 5 chars, each digit or A-Z, at least 4 digits
115func mbr_tok_code(t: *u8, ts: i64, te: i64) -> i64 {
116 if te - ts != 5 { return 0 }
117 var digits: i64 = 0
118 var i: i64 = ts
119 while i < te {
120 let d: i64 = mbr_is_digit(t[i])
121 if d == 1 { digits = digits + 1 } else {
122 if mbr_is_upper(t[i]) == 0 { return 0 }
123 }
124 i = i + 1
125 }
126 if digits >= 4 { return 1 }
127 return 0
128}
129
130// parse a whole document into ctx; returns number of claim lines found
131func mbr_parse(text: *u8, n: i64, ctx: *i64) -> i64 {
132 var i: i64 = 0
133 while i < n {
134 var e: i64 = i
135 var stop: i64 = 0
136 while stop == 0 {
137 if e >= n { stop = 1 } else {
138 if text[e] == (10 as u8) { stop = 1 } else { e = e + 1 }
139 }
140 }
141 // ---- scan tokens of line [i..e)
142 var codeS: i64 = 0 - 1
143 var codeE: i64 = 0 - 1
144 var date: i64 = 0 - 1
145 var units: i64 = 1
146 var m0: i64 = 0 - 1
147 var m1: i64 = 0 - 1
148 var m2: i64 = 0 - 1
149 var nm: i64 = 0
150 var p: i64 = i
151 while p < e {
152 var sk: i64 = 1
153 while sk == 1 {
154 if p >= e { sk = 0 } else {
155 let cw: i64 = text[p] as i64
156 if cw == 32 { p = p + 1 } else { if cw == 9 { p = p + 1 } else { if cw == 13 { p = p + 1 } else { sk = 0 } } }
157 }
158 }
159 if p < e {
160 var q: i64 = p
161 var run: i64 = 1
162 while run == 1 {
163 if q >= e { run = 0 } else {
164 let cq: i64 = text[q] as i64
165 if cq == 32 { run = 0 } else { if cq == 9 { run = 0 } else { if cq == 13 { run = 0 } else { q = q + 1 } } }
166 }
167 }
168 let mv: i64 = mbr_tok_money(text, p, q)
169 if mv >= 0 {
170 if nm == 0 { m0 = mv } else { if nm == 1 { m1 = mv } }
171 m2 = mv
172 nm = nm + 1
173 } else {
174 let dv: i64 = mbr_tok_date(text, p, q)
175 if dv > 0 { if date < 0 { date = dv } } else {
176 let uv: i64 = mbr_tok_units(text, p, q)
177 if uv > 0 { units = uv } else {
178 if codeS < 0 { if mbr_tok_code(text, p, q) == 1 { codeS = p; codeE = q } }
179 }
180 }
181 }
182 p = q
183 }
184 }
185 // ---- claim line iff code AND date AND money
186 if codeS >= 0 { if date > 0 { if nm >= 1 {
187 let k: i64 = ctx[0]
188 if k < ctx[8] {
189 let dst: *u8 = mbr_code_at(ctx, k)
190 var w: i64 = 0
191 var r: i64 = codeS
192 while r < codeE { dst[w] = text[r]; w = w + 1; r = r + 1 }
193 dst[w] = 0 as u8
194 let dp: *i64 = ctx[2] as *i64
195 dp[k] = date
196 let up: *i64 = ctx[3] as *i64
197 up[k] = units
198 let a0: *i64 = ctx[4] as *i64
199 a0[k] = m0
200 let a1: *i64 = ctx[5] as *i64
201 a1[k] = m1
202 let a2: *i64 = ctx[6] as *i64
203 a2[k] = m2
204 let np: *i64 = ctx[7] as *i64
205 np[k] = nm
206 ctx[0] = k + 1
207 }
208 } } }
209 i = e + 1
210 }
211 return ctx[0]
212}
213
214func mbr_code_eq(a: *u8, b: *u8) -> i64 {
215 var i: i64 = 0
216 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
217 if b[i] == (0 as u8) { return 1 }
218 return 0
219}
220
221// RECONCILE bill ctx vs EOB ctx. Consuming code+date matcher. Flags (parallel out arrays):
222// type 1 NOT_ON_EOB amount = bill charge (last money on the bill line)
223// type 2 BILLED_MISMATCH amount = |bill charge - EOB billed(first money)|
224// type 3 ABOVE_ALLOWED amount = bill charge - EOB allowed(second money) (needs EOB >=2 moneys)
225// Returns flag count. Zero flags on a fully-supported bill (the neg-control).
226func mbr_recon(bctx: *i64, ectx: *i64, ftyp: *i64, fidx: *i64, famt: *i64) -> i64 {
227 let bn: i64 = bctx[0]
228 let en: i64 = ectx[0]
229 let used: *i64 = sys_mmap(8 * (en + 1)) as *i64
230 var nf: i64 = 0
231 var i: i64 = 0
232 while i < bn {
233 let bcode: *u8 = mbr_code_at(bctx, i)
234 let bdate: i64 = mbr_geti(bctx, 2, i)
235 let bill: i64 = mbr_geti(bctx, 6, i)
236 var hit: i64 = 0 - 1
237 var j: i64 = 0
238 while j < en {
239 if used[j] == 0 {
240 if mbr_geti(ectx, 2, j) == bdate {
241 let ecode: *u8 = mbr_code_at(ectx, j)
242 if mbr_code_eq(bcode, ecode) == 1 { used[j] = 1; hit = j; j = en }
243 }
244 }
245 j = j + 1
246 }
247 if hit < 0 {
248 ftyp[nf] = 1
249 fidx[nf] = i
250 famt[nf] = bill
251 nf = nf + 1
252 } else {
253 let eb: i64 = mbr_geti(ectx, 4, hit)
254 if eb >= 0 { if bill != eb {
255 var d: i64 = bill - eb
256 if d < 0 { d = 0 - d }
257 ftyp[nf] = 2
258 fidx[nf] = i
259 famt[nf] = d
260 nf = nf + 1
261 } }
262 if mbr_geti(ectx, 7, hit) >= 2 {
263 let allow: i64 = mbr_geti(ectx, 5, hit)
264 if allow >= 0 { if bill > allow {
265 ftyp[nf] = 3
266 fidx[nf] = i
267 famt[nf] = bill - allow
268 nf = nf + 1
269 } }
270 }
271 }
272 i = i + 1
273 }
274 return nf
275}