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.
- disaster_fund_calculation_engine-0.1.0/PKG-INFO +296 -0
- disaster_fund_calculation_engine-0.1.0/README.md +273 -0
- disaster_fund_calculation_engine-0.1.0/pyproject.toml +37 -0
- disaster_fund_calculation_engine-0.1.0/setup.cfg +4 -0
- disaster_fund_calculation_engine-0.1.0/src/disaster_fund_calculation_engine.egg-info/PKG-INFO +296 -0
- disaster_fund_calculation_engine-0.1.0/src/disaster_fund_calculation_engine.egg-info/SOURCES.txt +14 -0
- disaster_fund_calculation_engine-0.1.0/src/disaster_fund_calculation_engine.egg-info/dependency_links.txt +1 -0
- disaster_fund_calculation_engine-0.1.0/src/disaster_fund_calculation_engine.egg-info/requires.txt +2 -0
- disaster_fund_calculation_engine-0.1.0/src/disaster_fund_calculation_engine.egg-info/top_level.txt +1 -0
- disaster_fund_calculation_engine-0.1.0/src/fm_calculation_engine/__init__.py +51 -0
- disaster_fund_calculation_engine-0.1.0/src/fm_calculation_engine/beneficiary.py +76 -0
- disaster_fund_calculation_engine-0.1.0/src/fm_calculation_engine/common.py +184 -0
- disaster_fund_calculation_engine-0.1.0/src/fm_calculation_engine/core.py +183 -0
- disaster_fund_calculation_engine-0.1.0/src/fm_calculation_engine/district.py +119 -0
- disaster_fund_calculation_engine-0.1.0/src/fm_calculation_engine/government.py +303 -0
- disaster_fund_calculation_engine-0.1.0/src/fm_calculation_engine/py.typed +0 -0
|
@@ -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"
|