-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathClaimEngine.sol
More file actions
126 lines (105 loc) · 4.71 KB
/
ClaimEngine.sol
File metadata and controls
126 lines (105 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.24;
import "./AccessControlled.sol";
import "./ProviderRegistry.sol";
import "./Enrollment.sol";
import "./Rules.sol";
import "./Bank.sol";
/**
* Minimal-leakage + immediate-settlement engine:
* - patientId (bytes32) is used for checks/counters but NEVER emitted and NOT stored in per-claim records.
* - year is YYYY only (no month/day).
* - per-code rules read from Rules: enabled, price, maxPerYear(0=unlimited).
* - auto-increment visit index per (patientId, year, code).
* - idempotency uses computed visit index to prevent accidental double-pays.
* - immediate Bank.pay() on success.
* - owner can pause/unpause submissions.
*/
contract ClaimEngine is AccessControlled {
ProviderRegistry public providers;
Enrollment public enrollment;
Rules public rules;
Bank public bank;
bool public paused;
// visit counts: patientId => year => code => count
mapping(bytes32 => mapping(uint16 => mapping(uint16 => uint32))) private _count;
uint256 private _nextId = 1;
mapping(bytes32 => bool) private _usedKey;
// Optional: lightweight lookup without leaking patientId
mapping(uint256 => bytes32) public claimKeyOf;
// NOTE: patientId intentionally omitted from events to minimize leakage
event ClaimPaid(
uint256 indexed id,
bytes32 indexed claimKey,
address indexed provider,
uint16 code,
uint16 year,
uint256 amount,
uint32 visitIndex
);
event ClaimRejected(bytes32 indexed claimKey, address indexed provider, uint16 code, uint16 year, string reason);
event PausedSet(bool paused);
constructor(ProviderRegistry _p, Enrollment _e, Rules _r, Bank _b) {
providers = _p;
enrollment = _e;
rules = _r;
bank = _b;
}
function setPaused(bool p) external onlyOwner {
paused = p;
emit PausedSet(p);
}
/**
* @param patientId Pseudonymous bytes32 known only to payer/provider off-chain
* @param code App-defined code (e.g., 1=Telehealth, 2=Annual)
* @param year YYYY only (1900..9999)
*/
function submit(bytes32 patientId, uint16 code, uint16 year) external {
require(!paused, "paused");
require(patientId != bytes32(0), "patientId=0");
require(year >= 1900 && year <= 9999, "bad year");
// Eligibility
if (!ProviderRegistry(providers).isActive(msg.sender, year)) {
return _rej(patientId, code, year, "provider inactive");
}
if (!Enrollment(enrollment).isCovered(patientId, year)) {
return _rej(patientId, code, year, "not covered");
}
// Rules / pricing
(bool enabled, uint256 price, uint16 maxPerYear) = rules.getRule(code);
if (!enabled || price == 0) {
return _rej(patientId, code, year, "code disabled/price=0");
}
// per-year cap: 0 = unlimited; else enforce maximum per patient/year/code
uint32 cur = _count[patientId][year][code];
if (maxPerYear != 0 && cur >= maxPerYear) {
return _rej(patientId, code, year, "max per year reached");
}
uint32 nextVisit = cur + 1;
// Idempotency keyed by computed next visit index (prevents double-submits of the same visit)
bytes32 key = keccak256(
abi.encodePacked(msg.sender, patientId, code, year, nextVisit, block.chainid, address(this))
);
if (_usedKey[key]) { emit ClaimRejected(key, msg.sender, code, year, "duplicate"); return; }
_usedKey[key] = true;
// Optional UX hint (soft reject instead of revert if unfunded)
try bank.vaultBalance() returns (uint256 bal) {
if (bal < price) { return _rej(patientId, code, year, "bank underfunded"); }
} catch {
// ignore hint failures; Bank.pay will enforce anyway
}
// Persist & bump counter (note: we do NOT store patientId in any per-claim record)
_count[patientId][year][code] = nextVisit;
uint256 id = _nextId++;
claimKeyOf[id] = key; // optional, public, non-PHI lookup
// Immediate settlement
bank.pay(msg.sender, price, id);
emit ClaimPaid(id, key, msg.sender, code, year, price, nextVisit);
}
// Build deterministic rejection key without advancing counters; avoids leaking patientId in logs.
function _rej(bytes32 patientId, uint16 code, uint16 year, string memory why) internal {
uint32 cur = _count[patientId][year][code];
bytes32 key = keccak256(abi.encodePacked(msg.sender, patientId, code, year, cur, block.chainid, address(this)));
emit ClaimRejected(key, msg.sender, code, year, why);
}
}