nx_ledes_lib.nx source
↩ module page · 124 lines · 5364 B
1// nx_ledes_lib.nx -- LEGAL SPEND: UTBMS coding + LEDES 1998B validation + outside-counsel guidelines.
2// The legal-ops category we had at zero. An invoice is not a document to read, it is a contract to
3// enforce: outside counsel guidelines say what may be billed, at what rate, in what increments, by whom.
4//
5// u2605BLOCK BILLING is the single largest source of disputed legal spend: several distinct tasks lumped
6// into one narrative with one time figure, so no line can be assessed on its own. It is detectable
7// structurally -- more than one UTBMS task code implicated by a single entry -- and it is rejected here
8// rather than merely flagged, because a flagged-but-payable line gets paid.
9//
10// u2605THE INCREMENT TOOTH: guidelines almost universally require tenth-of-an-hour billing. An entry in
11// quarter hours (0.25) silently inflates every short task by up to 50%. Encoded as an exact divisibility
12// check in HUNDREDTHS of an hour, so 0.1 is 10 and anything not a multiple of 10 fails.
13//
14// u2605FAIL-CLOSED: an entry with an UNRECOGNISED task or activity code is INVALID, never "assume billable".
15// A code nobody recognises is a line nobody reviewed.
16//
17// UTBMS ranges encoded as data-driven bounds (rule 11): litigation task codes L100-L700, counselling
18// C100-C500, activity codes A101-A114, expense codes E101-E124.
19// STRUCTURE: pure decision core, zero I/O. license_tier: ORIGINAL LIB.
20
21import "nx_matter_lib.nx"
22
23const LED_OK: i64 = 0
24const LED_BAD_TASK: i64 = 1
25const LED_BAD_ACTIVITY: i64 = 2
26const LED_BLOCK_BILLED: i64 = 3
27const LED_BAD_INCREMENT: i64 = 4
28const LED_RATE_EXCEEDED: i64 = 5
29const LED_NOT_BILLABLE: i64 = 6
30
31const LED_TASK_LIT_LO: i64 = 100
32const LED_TASK_LIT_HI: i64 = 700
33const LED_ACT_LO: i64 = 101
34const LED_ACT_HI: i64 = 114
35const LED_EXP_LO: i64 = 101
36const LED_EXP_HI: i64 = 124
37const LED_INCREMENT_HUNDREDTHS: i64 = 10
38
39func led_is1(v: i64) -> i64 {
40 if v == 1 { return 1 }
41 return 0
42}
43
44// a UTBMS litigation/counselling task code must fall in a recognised band
45func led_task_ok_pure(task: i64) -> i64 {
46 if task < LED_TASK_LIT_LO { return 0 }
47 if task > LED_TASK_LIT_HI { return 0 }
48 return 1
49}
50
51func led_activity_ok_pure(act: i64) -> i64 {
52 if act < LED_ACT_LO { return 0 }
53 if act > LED_ACT_HI { return 0 }
54 return 1
55}
56
57func led_expense_ok_pure(exp: i64) -> i64 {
58 if exp < LED_EXP_LO { return 0 }
59 if exp > LED_EXP_HI { return 0 }
60 return 1
61}
62
63// u2605more than ONE task code implicated by a single narrative = block billing
64func led_block_billed_pure(task_count: i64) -> i64 {
65 if task_count > 1 { return 1 }
66 return 0
67}
68
69// hours expressed in HUNDREDTHS (0.1h = 10). Must be a positive multiple of the increment.
70func led_increment_ok_pure(hours_hundredths: i64) -> i64 {
71 if hours_hundredths <= 0 { return 0 }
72 if (hours_hundredths % LED_INCREMENT_HUNDREDTHS) != 0 { return 0 }
73 return 1
74}
75
76// rate in cents/hour against the guideline cap; a cap of 0 means "no cap agreed" and is NOT a free pass,
77// it is an unenforceable guideline -- so it fails closed.
78func led_rate_ok_pure(rate_cents: i64, cap_cents: i64) -> i64 {
79 if rate_cents <= 0 { return 0 }
80 if cap_cents <= 0 { return 0 }
81 if rate_cents > cap_cents { return 0 }
82 return 1
83}
84
85// u2605THE ENTRY VERDICT, first failure wins so the reason is actionable rather than a boolean.
86func led_entry_check_pure(task: i64, act: i64, task_count: i64, hours_hundredths: i64, rate_cents: i64, cap_cents: i64, billable: i64) -> i64 {
87 if led_is1(billable) == 0 { return LED_NOT_BILLABLE }
88 if led_task_ok_pure(task) == 0 { return LED_BAD_TASK }
89 if led_activity_ok_pure(act) == 0 { return LED_BAD_ACTIVITY }
90 if led_block_billed_pure(task_count) == 1 { return LED_BLOCK_BILLED }
91 if led_increment_ok_pure(hours_hundredths) == 0 { return LED_BAD_INCREMENT }
92 if led_rate_ok_pure(rate_cents, cap_cents) == 0 { return LED_RATE_EXCEEDED }
93 return LED_OK
94}
95
96func led_entry_payable_pure(verdict: i64) -> i64 {
97 if verdict == LED_OK { return 1 }
98 return 0
99}
100
101// line amount in cents = hours(hundredths) * rate(cents/hr) / 100, only for a payable line.
102func led_line_amount_pure(verdict: i64, hours_hundredths: i64, rate_cents: i64) -> i64 {
103 if led_entry_payable_pure(verdict) == 0 { return 0 }
104 return (hours_hundredths * rate_cents) / 100
105}
106
107// an invoice is payable only when EVERY line is; one bad line does not silently reduce the total.
108func led_invoice_clean_pure(total_lines: i64, rejected_lines: i64) -> i64 {
109 if total_lines <= 0 { return 0 }
110 if rejected_lines != 0 { return 0 }
111 return 1
112}
113
114func led_verdict_label(v: i64, out: *u8) -> i64 {
115 if v == LED_BAD_TASK { mt_catcopy(out, 0, "BAD-TASK-CODE" as *u8); out[13] = 0 as u8; return 13 }
116 if v == LED_BAD_ACTIVITY { mt_catcopy(out, 0, "BAD-ACTIVITY-CODE" as *u8); out[17] = 0 as u8; return 17 }
117 if v == LED_BLOCK_BILLED { mt_catcopy(out, 0, "BLOCK-BILLED" as *u8); out[12] = 0 as u8; return 12 }
118 if v == LED_BAD_INCREMENT { mt_catcopy(out, 0, "BAD-INCREMENT" as *u8); out[13] = 0 as u8; return 13 }
119 if v == LED_RATE_EXCEEDED { mt_catcopy(out, 0, "RATE-EXCEEDED" as *u8); out[13] = 0 as u8; return 13 }
120 if v == LED_NOT_BILLABLE { mt_catcopy(out, 0, "NOT-BILLABLE" as *u8); out[12] = 0 as u8; return 12 }
121 mt_catcopy(out, 0, "OK" as *u8)
122 out[2] = 0 as u8
123 return 2
124}