disaster-fund-calculation-engine 0.1.0__tar.gz

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.
@@ -0,0 +1,296 @@
1
+ Metadata-Version: 2.4
2
+ Name: disaster-fund-calculation-engine
3
+ Version: 0.1.0
4
+ Summary: Disaster Reconstruction & Relief Fund Calculation Engine: Multi-Tranche Liabilities, Donation Offsets, and District Pooling
5
+ Author-email: Pujan Pandey <arghakhanchipujan@gmail.com>
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/arghakhanchipujan/Fund-Management
8
+ Keywords: disaster-relief,fund-management,reconstruction,nepal,grants,calculation-engine,tranches
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
18
+ Classifier: Development Status :: 3 - Alpha
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: disaster-fund-plan>=0.1.0
22
+ Requires-Dist: pydantic>=2
23
+
24
+ # disaster-fund-calculation-engine
25
+
26
+ **Turn a validated disaster plan plus beneficiary counts into exact rupee figures: what a household receives, what each government owes, and what each district collects and forwards.**
27
+
28
+ It is the calculation half of the system. The plan itself (structure, grant rates, liability split) comes from [`disaster-fund-plan`](https://pypi.org/project/disaster-fund-plan/), which is installed automatically.
29
+
30
+ - Python 3.10 or newer
31
+ - Dependencies: `disaster-fund-plan`, `pydantic>=2`
32
+ - Import name: `fm_calculation_engine` (the PyPI name is `disaster-fund-calculation-engine`)
33
+
34
+ ```bash
35
+ pip install disaster-fund-calculation-engine
36
+ ```
37
+
38
+ ---
39
+
40
+ ## The three outputs at a glance
41
+
42
+ | Question you are asking | Function | Who asks it |
43
+ | --- | --- | --- |
44
+ | *"What does one household receive, tranche by tranche?"* | `receivable_amounts(plan, profile)` | Caseworker, beneficiary-facing app |
45
+ | *"How much does the Federal / a Province / a Local government owe, after donations?"* | `calculate_government_liability(plan, profile, counts)` | Finance office of that government |
46
+ | *"How much must a district collect from each level, and how much does it forward to each local government?"* | `calculate_district_fund(plan, profile, counts)` | District coordination office |
47
+
48
+ All three read the **same** internal schedule, so they can never disagree with each other (see [Guarantees](#guarantees)).
49
+
50
+ ## Inputs
51
+
52
+ **1. The plan**: loaded with `disaster-fund-plan`:
53
+
54
+ ```python
55
+ from fm_plan import load_plan
56
+ plan = load_plan("example_plan.json")
57
+ ```
58
+
59
+ **2. Beneficiary counts** (outputs 2 and 3): how many households fall in each category. A list of dictionaries:
60
+
61
+ ```python
62
+ counts = [
63
+ {"district": "Gorkha", "local_level": "Gorkha Municipality",
64
+ "disaster_type": "Earthquake", "solution_type": "Reconstruction", "count": 10},
65
+ {"district": "Sindhupalchok", "local_level": "Melamchi Municipality",
66
+ "disaster_type": "Earthquake", "solution_type": "Reconstruction", "count": 20},
67
+ {"district": "Sindhupalchok", "local_level": "Helambu Rural Municipality",
68
+ "disaster_type": "Earthquake", "solution_type": "Retrofitting", "count": 5},
69
+ ]
70
+ ```
71
+
72
+ | Field | Required | Notes |
73
+ | --- | --- | --- |
74
+ | `district` | yes | Name **or code** (`"Sindhupalchok"` or `"23"`) |
75
+ | `disaster_type`, `solution_type` | yes | Must exist in the plan; capital letters do not matter |
76
+ | `count` | yes | Number of households, `0` or more |
77
+ | `local_level` | no | Name or code. If omitted, see [use case 6](#6-counts-without-a-local-level) |
78
+ | `region` | no | Taken from the plan's district. If you supply it, it must match the plan |
79
+ | `district_code`, `local_level_code` | no | Optional cross-check (non-empty strings). If given, they must match the plan's code for that district / local level, otherwise the row is rejected (a local-level code needs a `local_level`) |
80
+
81
+ **3. A profile** for the question you are asking (shown in each use case below). Every profile carries the `plan_id`, which must match the loaded plan: this stops results being calculated against the wrong plan version.
82
+
83
+ ---
84
+
85
+ ## Use cases
86
+
87
+ ### 1. What does one household receive?
88
+
89
+ Given a household's region, disaster type and solution type, get its tranche schedule.
90
+
91
+ ```python
92
+ from fm_calculation_engine import receivable_amounts
93
+
94
+ out = receivable_amounts(plan, {
95
+ "plan_id": plan.plan_id,
96
+ "region": "Pahad",
97
+ "disaster_type": "Earthquake",
98
+ "solution_type": "Reconstruction",
99
+ })
100
+
101
+ print(out.total) # 300000
102
+ for t in out.tranches:
103
+ print(t.tranche, t.stage, t.amount)
104
+ # 1 shelter 15000
105
+ # 2 shelter 15000
106
+ # 3 main 100000
107
+ # 4 main 100000
108
+ # 5 main 70000
109
+ ```
110
+
111
+ ### 2. What does a government owe? (Federal, Province or Local)
112
+
113
+ The same function works for every level. The engine works out which level you mean from the name or code.
114
+
115
+ ```python
116
+ from fm_calculation_engine import calculate_government_liability
117
+
118
+ fed = calculate_government_liability(
119
+ plan,
120
+ {"plan_id": plan.plan_id, "gov_entity_name": "Government of Nepal"}, # or "FED"
121
+ counts,
122
+ )
123
+ print(fed.gov_type, fed.liability_percentage) # federal 50.0
124
+ print(fed.total_gross_liability) # 4750000
125
+ print(fed.total_net_payable) # 4750000 (no donations given)
126
+ ```
127
+
128
+ Other levels, using the plan from the example file:
129
+
130
+ | `gov_entity_name` | Resolves to | Pays |
131
+ | --- | --- | --- |
132
+ | `"Government of Nepal"` or `"FED"` | federal | 50% of every district's cost |
133
+ | `"Gandaki Province"` or `"P4"` | province | 30%, **only** for districts inside that province (Gorkha) |
134
+ | `"Melamchi Municipality"` or `"23001"` | local | 20% of its district's cost, split between the district's local levels in proportion to what each needs |
135
+
136
+ Each result has a per-tranche, per-district breakdown (`fed.tranches[i].districts`) and a summary of who the beneficiaries are (`fed.beneficiary_summary`).
137
+
138
+ ### 3. Apply donations, with automatic rollover
139
+
140
+ INGO, NGO and other donors give money to a government. It reduces that government's **cash outflow**, starting at tranche 1; whatever is not used rolls over to the next tranche.
141
+
142
+ ```python
143
+ fed = calculate_government_liability(
144
+ plan,
145
+ {
146
+ "plan_id": plan.plan_id,
147
+ "gov_entity_name": "Government of Nepal",
148
+ "donations": [
149
+ {"donor_name": "Red Cross", "amount": 250000, "received_date": "2015-10-01"},
150
+ {"donor_name": "UNICEF", "amount": 150000, "received_date": "2015-10-05"},
151
+ ],
152
+ },
153
+ counts,
154
+ )
155
+ for t in fed.tranches[:3]:
156
+ print(t.tranche, t.gross_liability, t.donation_applied, t.net_payable, t.remaining_donation)
157
+ ```
158
+
159
+ ```text
160
+ tranche gross donation applied net payable donation left
161
+ 1 350,000 350,000 0 50,000 ← 400,000 donated, tranche 1 consumes 350,000
162
+ 2 350,000 50,000 300,000 0 ← the 50,000 left over rolls into tranche 2
163
+ 3 1,500,000 0 1,500,000 0 ← donations are used up
164
+ ```
165
+
166
+ Totals: Rs. 400,000 received, Rs. 4,750,000 gross, **Rs. 4,350,000 net cash payable**. The relief inside each tranche is shared across districts in proportion to what each district needs (`tranches[i].districts[j].donation_offset`).
167
+
168
+ Notes: `received_date` is optional and only affects the order donations are listed (oldest first). It does not decide *which tranche* a donation can pay for, because the plan has no tranche due dates.
169
+
170
+ ### 4. What must a district collect, and whom does it forward to?
171
+
172
+ A district office needs two answers per tranche: how much to **collect** from Federal, Province and Local, and how much to **forward** to each local government.
173
+
174
+ ```python
175
+ from fm_calculation_engine import calculate_district_fund
176
+
177
+ d = calculate_district_fund(
178
+ plan,
179
+ {"plan_id": plan.plan_id, "district_name": "Sindhupalchok"}, # or "23"
180
+ counts,
181
+ )
182
+ print(d.province_name, d.total_beneficiaries, d.grand_total) # Bagmati Province 25 6500000
183
+
184
+ t1 = d.tranches[0]
185
+ print(t1.collectible) # federal=275000 province=165000 local=110000 total=550000
186
+ for x in t1.disbursements:
187
+ print(x.local_gov, x.amount)
188
+ # Melamchi Municipality 300000
189
+ # Helambu Rural Municipality 250000
190
+ ```
191
+
192
+ Tranche 1 here is 550,000 in total: Rs. 275,000 (50%) from Federal, Rs. 165,000 (30%) from the Province and Rs. 110,000 (20%) from Local, which is then forwarded as 300,000 and 250,000 to the two local governments according to their own beneficiaries.
193
+
194
+ ### 5. Reconcile everything (audit and sanity check)
195
+
196
+ Because all outputs come from one schedule, they must add up. This is a useful check for auditors and for your own integration tests:
197
+
198
+ ```python
199
+ levels = ["FED", "P3", "P4", "Gorkha Municipality", "Melamchi Municipality", "Helambu Rural Municipality"]
200
+ total_by_government = sum(
201
+ calculate_government_liability(plan, {"plan_id": plan.plan_id, "gov_entity_name": g}, counts).total_gross_liability
202
+ for g in levels
203
+ )
204
+ total_by_district = sum(
205
+ calculate_district_fund(plan, {"plan_id": plan.plan_id, "district_name": d}, counts).grand_total
206
+ for d in ("Gorkha", "Sindhupalchok")
207
+ )
208
+ assert total_by_government == total_by_district == 9_500_000
209
+ ```
210
+
211
+ ### 6. Counts without a local level
212
+
213
+ Sometimes only district-level totals are known. Leave `local_level` out:
214
+
215
+ ```python
216
+ calculate_district_fund(plan, {"plan_id": plan.plan_id, "district_name": "23"},
217
+ [{"district": "23", "disaster_type": "Earthquake", "solution_type": "Reconstruction", "count": 10}])
218
+ # tranche 1 -> Melamchi Municipality 75000, Helambu Rural Municipality 75000
219
+ ```
220
+
221
+ The money for such households is shared **equally** between the district's local levels (odd rupees go to the first ones in the plan). For a local government's own report, these households are shown separately as `unassigned_beneficiaries` and are not included in `total_beneficiaries`.
222
+
223
+ ### 7. Catch bad input with readable errors
224
+
225
+ Every problem in the input is reported at once, in plain words, as an `EngineInputError`:
226
+
227
+ ```python
228
+ from fm_calculation_engine import EngineInputError
229
+
230
+ try:
231
+ calculate_district_fund(plan, {"plan_id": plan.plan_id, "district_name": "Gorkha"}, bad_counts)
232
+ except EngineInputError as error:
233
+ print(error)
234
+ ```
235
+
236
+ ```text
237
+ 3 problem(s) in beneficiary counts:
238
+ - item 0: district 'Kathmandu' is not in plan's districts ['Gorkha', 'Sindhupalchok']
239
+ - item 0: disaster_type 'Flood' is not in plan's disaster types ['Earthquake']
240
+ - item 1: region 'Terai' does not match district 'Gorkha', which is in region 'Pahad' in this plan
241
+ ```
242
+
243
+ You can also validate a count list on its own, before calculating, and get it back normalised to the plan's spelling and codes:
244
+
245
+ ```python
246
+ from fm_calculation_engine import validate_beneficiary_counts
247
+ clean = validate_beneficiary_counts(plan, counts)
248
+ ```
249
+
250
+ ### 8. A local government whose name exists in two districts
251
+
252
+ Nepal has repeated municipality names. If a local government's name appears in more than one district, the engine refuses to guess; say which district you mean:
253
+
254
+ ```python
255
+ calculate_government_liability(plan, {
256
+ "plan_id": plan.plan_id,
257
+ "gov_entity_name": "Some Municipality",
258
+ "district": "Sindhupalchok",
259
+ }, counts)
260
+ ```
261
+
262
+ (`district` is only accepted for local governments. Using the official code, such as `"23001"`, avoids the ambiguity altogether.)
263
+
264
+ ---
265
+
266
+ ## Guarantees
267
+
268
+ - **Exact rupees, no drift.** Every split (liability shares, local-level shares, donation offsets) uses the *largest remainder method* on whole rupees: each part gets what it is owed, and leftover rupees go to the parts with the largest fractions. The parts always add up to the total; `round()` and floating point are never used for money.
269
+ - **One schedule per district.** District total = Federal + Province + Local share, and the local share = the sum of the shares given to its local levels, exactly. Outputs 2 and 3 therefore always reconcile.
270
+ - **Deterministic.** The same input always gives the same output (ties are broken by plan order).
271
+ - **Wrong plan is rejected.** A `plan_id` that does not match the loaded plan is an error.
272
+ - **Names or codes everywhere.** Districts, provinces, local levels and the Federal government can all be given by name or official code, in any capitalisation.
273
+
274
+ ## API summary
275
+
276
+ | Name | Purpose |
277
+ | --- | --- |
278
+ | `receivable_amounts(plan, profile)` → `ReceivableAmounts` | Output 1: tranche schedule for one household |
279
+ | `calculate_government_liability(plan, profile, counts)` → `GovernmentLiabilityOutput` | Output 2: what a government owes, with donation offsets and rollover |
280
+ | `calculate_district_fund(plan, profile, counts)` → `DistrictOutput` | Output 3: what a district collects and forwards |
281
+ | `validate_beneficiary_counts(plan, counts)` | Check counts against the plan; returns normalised `BeneficiaryCount` objects |
282
+ | `EngineInputError` | Raised for any invalid input; the message lists every problem |
283
+ | `BeneficiaryProfile`, `GovEntityProfile`, `DistrictProfile`, `BeneficiaryCount`, `Donation` | Input models (a plain `dict` with the same keys works too) |
284
+
285
+ Profiles and counts can be passed as dictionaries or as these model objects. Unknown keys are rejected, so a typo is an error rather than being silently ignored.
286
+
287
+ ## Limits
288
+
289
+ - Amounts are whole rupees.
290
+ - The Federal/Province/Local split is the same for every tranche and every stage (shelter and main).
291
+ - Donations are pooled per government and used from tranche 1 onward; they are not tied to a particular tranche or district.
292
+ - A tranche round exists up to the longest grant in the plan. A grant with fewer tranches simply has Rs. 0 in the later rounds.
293
+
294
+ ## License
295
+
296
+ MIT
@@ -0,0 +1,273 @@
1
+ # disaster-fund-calculation-engine
2
+
3
+ **Turn a validated disaster plan plus beneficiary counts into exact rupee figures: what a household receives, what each government owes, and what each district collects and forwards.**
4
+
5
+ It is the calculation half of the system. The plan itself (structure, grant rates, liability split) comes from [`disaster-fund-plan`](https://pypi.org/project/disaster-fund-plan/), which is installed automatically.
6
+
7
+ - Python 3.10 or newer
8
+ - Dependencies: `disaster-fund-plan`, `pydantic>=2`
9
+ - Import name: `fm_calculation_engine` (the PyPI name is `disaster-fund-calculation-engine`)
10
+
11
+ ```bash
12
+ pip install disaster-fund-calculation-engine
13
+ ```
14
+
15
+ ---
16
+
17
+ ## The three outputs at a glance
18
+
19
+ | Question you are asking | Function | Who asks it |
20
+ | --- | --- | --- |
21
+ | *"What does one household receive, tranche by tranche?"* | `receivable_amounts(plan, profile)` | Caseworker, beneficiary-facing app |
22
+ | *"How much does the Federal / a Province / a Local government owe, after donations?"* | `calculate_government_liability(plan, profile, counts)` | Finance office of that government |
23
+ | *"How much must a district collect from each level, and how much does it forward to each local government?"* | `calculate_district_fund(plan, profile, counts)` | District coordination office |
24
+
25
+ All three read the **same** internal schedule, so they can never disagree with each other (see [Guarantees](#guarantees)).
26
+
27
+ ## Inputs
28
+
29
+ **1. The plan**: loaded with `disaster-fund-plan`:
30
+
31
+ ```python
32
+ from fm_plan import load_plan
33
+ plan = load_plan("example_plan.json")
34
+ ```
35
+
36
+ **2. Beneficiary counts** (outputs 2 and 3): how many households fall in each category. A list of dictionaries:
37
+
38
+ ```python
39
+ counts = [
40
+ {"district": "Gorkha", "local_level": "Gorkha Municipality",
41
+ "disaster_type": "Earthquake", "solution_type": "Reconstruction", "count": 10},
42
+ {"district": "Sindhupalchok", "local_level": "Melamchi Municipality",
43
+ "disaster_type": "Earthquake", "solution_type": "Reconstruction", "count": 20},
44
+ {"district": "Sindhupalchok", "local_level": "Helambu Rural Municipality",
45
+ "disaster_type": "Earthquake", "solution_type": "Retrofitting", "count": 5},
46
+ ]
47
+ ```
48
+
49
+ | Field | Required | Notes |
50
+ | --- | --- | --- |
51
+ | `district` | yes | Name **or code** (`"Sindhupalchok"` or `"23"`) |
52
+ | `disaster_type`, `solution_type` | yes | Must exist in the plan; capital letters do not matter |
53
+ | `count` | yes | Number of households, `0` or more |
54
+ | `local_level` | no | Name or code. If omitted, see [use case 6](#6-counts-without-a-local-level) |
55
+ | `region` | no | Taken from the plan's district. If you supply it, it must match the plan |
56
+ | `district_code`, `local_level_code` | no | Optional cross-check (non-empty strings). If given, they must match the plan's code for that district / local level, otherwise the row is rejected (a local-level code needs a `local_level`) |
57
+
58
+ **3. A profile** for the question you are asking (shown in each use case below). Every profile carries the `plan_id`, which must match the loaded plan: this stops results being calculated against the wrong plan version.
59
+
60
+ ---
61
+
62
+ ## Use cases
63
+
64
+ ### 1. What does one household receive?
65
+
66
+ Given a household's region, disaster type and solution type, get its tranche schedule.
67
+
68
+ ```python
69
+ from fm_calculation_engine import receivable_amounts
70
+
71
+ out = receivable_amounts(plan, {
72
+ "plan_id": plan.plan_id,
73
+ "region": "Pahad",
74
+ "disaster_type": "Earthquake",
75
+ "solution_type": "Reconstruction",
76
+ })
77
+
78
+ print(out.total) # 300000
79
+ for t in out.tranches:
80
+ print(t.tranche, t.stage, t.amount)
81
+ # 1 shelter 15000
82
+ # 2 shelter 15000
83
+ # 3 main 100000
84
+ # 4 main 100000
85
+ # 5 main 70000
86
+ ```
87
+
88
+ ### 2. What does a government owe? (Federal, Province or Local)
89
+
90
+ The same function works for every level. The engine works out which level you mean from the name or code.
91
+
92
+ ```python
93
+ from fm_calculation_engine import calculate_government_liability
94
+
95
+ fed = calculate_government_liability(
96
+ plan,
97
+ {"plan_id": plan.plan_id, "gov_entity_name": "Government of Nepal"}, # or "FED"
98
+ counts,
99
+ )
100
+ print(fed.gov_type, fed.liability_percentage) # federal 50.0
101
+ print(fed.total_gross_liability) # 4750000
102
+ print(fed.total_net_payable) # 4750000 (no donations given)
103
+ ```
104
+
105
+ Other levels, using the plan from the example file:
106
+
107
+ | `gov_entity_name` | Resolves to | Pays |
108
+ | --- | --- | --- |
109
+ | `"Government of Nepal"` or `"FED"` | federal | 50% of every district's cost |
110
+ | `"Gandaki Province"` or `"P4"` | province | 30%, **only** for districts inside that province (Gorkha) |
111
+ | `"Melamchi Municipality"` or `"23001"` | local | 20% of its district's cost, split between the district's local levels in proportion to what each needs |
112
+
113
+ Each result has a per-tranche, per-district breakdown (`fed.tranches[i].districts`) and a summary of who the beneficiaries are (`fed.beneficiary_summary`).
114
+
115
+ ### 3. Apply donations, with automatic rollover
116
+
117
+ INGO, NGO and other donors give money to a government. It reduces that government's **cash outflow**, starting at tranche 1; whatever is not used rolls over to the next tranche.
118
+
119
+ ```python
120
+ fed = calculate_government_liability(
121
+ plan,
122
+ {
123
+ "plan_id": plan.plan_id,
124
+ "gov_entity_name": "Government of Nepal",
125
+ "donations": [
126
+ {"donor_name": "Red Cross", "amount": 250000, "received_date": "2015-10-01"},
127
+ {"donor_name": "UNICEF", "amount": 150000, "received_date": "2015-10-05"},
128
+ ],
129
+ },
130
+ counts,
131
+ )
132
+ for t in fed.tranches[:3]:
133
+ print(t.tranche, t.gross_liability, t.donation_applied, t.net_payable, t.remaining_donation)
134
+ ```
135
+
136
+ ```text
137
+ tranche gross donation applied net payable donation left
138
+ 1 350,000 350,000 0 50,000 ← 400,000 donated, tranche 1 consumes 350,000
139
+ 2 350,000 50,000 300,000 0 ← the 50,000 left over rolls into tranche 2
140
+ 3 1,500,000 0 1,500,000 0 ← donations are used up
141
+ ```
142
+
143
+ Totals: Rs. 400,000 received, Rs. 4,750,000 gross, **Rs. 4,350,000 net cash payable**. The relief inside each tranche is shared across districts in proportion to what each district needs (`tranches[i].districts[j].donation_offset`).
144
+
145
+ Notes: `received_date` is optional and only affects the order donations are listed (oldest first). It does not decide *which tranche* a donation can pay for, because the plan has no tranche due dates.
146
+
147
+ ### 4. What must a district collect, and whom does it forward to?
148
+
149
+ A district office needs two answers per tranche: how much to **collect** from Federal, Province and Local, and how much to **forward** to each local government.
150
+
151
+ ```python
152
+ from fm_calculation_engine import calculate_district_fund
153
+
154
+ d = calculate_district_fund(
155
+ plan,
156
+ {"plan_id": plan.plan_id, "district_name": "Sindhupalchok"}, # or "23"
157
+ counts,
158
+ )
159
+ print(d.province_name, d.total_beneficiaries, d.grand_total) # Bagmati Province 25 6500000
160
+
161
+ t1 = d.tranches[0]
162
+ print(t1.collectible) # federal=275000 province=165000 local=110000 total=550000
163
+ for x in t1.disbursements:
164
+ print(x.local_gov, x.amount)
165
+ # Melamchi Municipality 300000
166
+ # Helambu Rural Municipality 250000
167
+ ```
168
+
169
+ Tranche 1 here is 550,000 in total: Rs. 275,000 (50%) from Federal, Rs. 165,000 (30%) from the Province and Rs. 110,000 (20%) from Local, which is then forwarded as 300,000 and 250,000 to the two local governments according to their own beneficiaries.
170
+
171
+ ### 5. Reconcile everything (audit and sanity check)
172
+
173
+ Because all outputs come from one schedule, they must add up. This is a useful check for auditors and for your own integration tests:
174
+
175
+ ```python
176
+ levels = ["FED", "P3", "P4", "Gorkha Municipality", "Melamchi Municipality", "Helambu Rural Municipality"]
177
+ total_by_government = sum(
178
+ calculate_government_liability(plan, {"plan_id": plan.plan_id, "gov_entity_name": g}, counts).total_gross_liability
179
+ for g in levels
180
+ )
181
+ total_by_district = sum(
182
+ calculate_district_fund(plan, {"plan_id": plan.plan_id, "district_name": d}, counts).grand_total
183
+ for d in ("Gorkha", "Sindhupalchok")
184
+ )
185
+ assert total_by_government == total_by_district == 9_500_000
186
+ ```
187
+
188
+ ### 6. Counts without a local level
189
+
190
+ Sometimes only district-level totals are known. Leave `local_level` out:
191
+
192
+ ```python
193
+ calculate_district_fund(plan, {"plan_id": plan.plan_id, "district_name": "23"},
194
+ [{"district": "23", "disaster_type": "Earthquake", "solution_type": "Reconstruction", "count": 10}])
195
+ # tranche 1 -> Melamchi Municipality 75000, Helambu Rural Municipality 75000
196
+ ```
197
+
198
+ The money for such households is shared **equally** between the district's local levels (odd rupees go to the first ones in the plan). For a local government's own report, these households are shown separately as `unassigned_beneficiaries` and are not included in `total_beneficiaries`.
199
+
200
+ ### 7. Catch bad input with readable errors
201
+
202
+ Every problem in the input is reported at once, in plain words, as an `EngineInputError`:
203
+
204
+ ```python
205
+ from fm_calculation_engine import EngineInputError
206
+
207
+ try:
208
+ calculate_district_fund(plan, {"plan_id": plan.plan_id, "district_name": "Gorkha"}, bad_counts)
209
+ except EngineInputError as error:
210
+ print(error)
211
+ ```
212
+
213
+ ```text
214
+ 3 problem(s) in beneficiary counts:
215
+ - item 0: district 'Kathmandu' is not in plan's districts ['Gorkha', 'Sindhupalchok']
216
+ - item 0: disaster_type 'Flood' is not in plan's disaster types ['Earthquake']
217
+ - item 1: region 'Terai' does not match district 'Gorkha', which is in region 'Pahad' in this plan
218
+ ```
219
+
220
+ You can also validate a count list on its own, before calculating, and get it back normalised to the plan's spelling and codes:
221
+
222
+ ```python
223
+ from fm_calculation_engine import validate_beneficiary_counts
224
+ clean = validate_beneficiary_counts(plan, counts)
225
+ ```
226
+
227
+ ### 8. A local government whose name exists in two districts
228
+
229
+ Nepal has repeated municipality names. If a local government's name appears in more than one district, the engine refuses to guess; say which district you mean:
230
+
231
+ ```python
232
+ calculate_government_liability(plan, {
233
+ "plan_id": plan.plan_id,
234
+ "gov_entity_name": "Some Municipality",
235
+ "district": "Sindhupalchok",
236
+ }, counts)
237
+ ```
238
+
239
+ (`district` is only accepted for local governments. Using the official code, such as `"23001"`, avoids the ambiguity altogether.)
240
+
241
+ ---
242
+
243
+ ## Guarantees
244
+
245
+ - **Exact rupees, no drift.** Every split (liability shares, local-level shares, donation offsets) uses the *largest remainder method* on whole rupees: each part gets what it is owed, and leftover rupees go to the parts with the largest fractions. The parts always add up to the total; `round()` and floating point are never used for money.
246
+ - **One schedule per district.** District total = Federal + Province + Local share, and the local share = the sum of the shares given to its local levels, exactly. Outputs 2 and 3 therefore always reconcile.
247
+ - **Deterministic.** The same input always gives the same output (ties are broken by plan order).
248
+ - **Wrong plan is rejected.** A `plan_id` that does not match the loaded plan is an error.
249
+ - **Names or codes everywhere.** Districts, provinces, local levels and the Federal government can all be given by name or official code, in any capitalisation.
250
+
251
+ ## API summary
252
+
253
+ | Name | Purpose |
254
+ | --- | --- |
255
+ | `receivable_amounts(plan, profile)` → `ReceivableAmounts` | Output 1: tranche schedule for one household |
256
+ | `calculate_government_liability(plan, profile, counts)` → `GovernmentLiabilityOutput` | Output 2: what a government owes, with donation offsets and rollover |
257
+ | `calculate_district_fund(plan, profile, counts)` → `DistrictOutput` | Output 3: what a district collects and forwards |
258
+ | `validate_beneficiary_counts(plan, counts)` | Check counts against the plan; returns normalised `BeneficiaryCount` objects |
259
+ | `EngineInputError` | Raised for any invalid input; the message lists every problem |
260
+ | `BeneficiaryProfile`, `GovEntityProfile`, `DistrictProfile`, `BeneficiaryCount`, `Donation` | Input models (a plain `dict` with the same keys works too) |
261
+
262
+ Profiles and counts can be passed as dictionaries or as these model objects. Unknown keys are rejected, so a typo is an error rather than being silently ignored.
263
+
264
+ ## Limits
265
+
266
+ - Amounts are whole rupees.
267
+ - The Federal/Province/Local split is the same for every tranche and every stage (shelter and main).
268
+ - Donations are pooled per government and used from tranche 1 onward; they are not tied to a particular tranche or district.
269
+ - A tranche round exists up to the longest grant in the plan. A grant with fewer tranches simply has Rs. 0 in the later rounds.
270
+
271
+ ## License
272
+
273
+ MIT
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "disaster-fund-calculation-engine"
7
+ version = "0.1.0"
8
+ description = "Disaster Reconstruction & Relief Fund Calculation Engine: Multi-Tranche Liabilities, Donation Offsets, and District Pooling"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Pujan Pandey", email = "arghakhanchipujan@gmail.com" }]
13
+ keywords = ["disaster-relief", "fund-management", "reconstruction", "nepal", "grants", "calculation-engine", "tranches"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Topic :: Office/Business :: Financial :: Accounting",
24
+ "Development Status :: 3 - Alpha",
25
+ ]
26
+
27
+ dependencies = [
28
+ "disaster-fund-plan>=0.1.0",
29
+ "pydantic>=2",
30
+ ]
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+
35
+
36
+ [project.urls]
37
+ Repository = "https://github.com/arghakhanchipujan/Fund-Management"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+