repaykit 1.0.0__py3-none-any.whl

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.
Files changed (48) hide show
  1. repaykit/__init__.py +18 -0
  2. repaykit/_serialization.py +28 -0
  3. repaykit/account.py +98 -0
  4. repaykit/accruals/__init__.py +8 -0
  5. repaykit/accruals/base.py +25 -0
  6. repaykit/accruals/daily.py +18 -0
  7. repaykit/accruals/flat.py +14 -0
  8. repaykit/accruals/periodic.py +12 -0
  9. repaykit/backends/README.md +12 -0
  10. repaykit/enums.py +21 -0
  11. repaykit/exceptions.py +21 -0
  12. repaykit/exporters/__init__.py +12 -0
  13. repaykit/exporters/csv.py +35 -0
  14. repaykit/exporters/dicts.py +19 -0
  15. repaykit/ledger/__init__.py +7 -0
  16. repaykit/ledger/account_statement.py +5 -0
  17. repaykit/ledger/payment.py +25 -0
  18. repaykit/ledger/transaction.py +17 -0
  19. repaykit/loan.py +157 -0
  20. repaykit/methods/__init__.py +15 -0
  21. repaykit/methods/base.py +46 -0
  22. repaykit/methods/constant_principal.py +77 -0
  23. repaykit/methods/flat_rate.py +93 -0
  24. repaykit/methods/reducing_balance.py +85 -0
  25. repaykit/methods/sum_of_digits.py +96 -0
  26. repaykit/money.py +37 -0
  27. repaykit/policies/__init__.py +14 -0
  28. repaykit/policies/allocation.py +21 -0
  29. repaykit/policies/late_charge.py +63 -0
  30. repaykit/policies/rounding.py +36 -0
  31. repaykit/policies/settlement.py +66 -0
  32. repaykit/py.typed +1 -0
  33. repaykit/results.py +58 -0
  34. repaykit/schedules/__init__.py +21 -0
  35. repaykit/schedules/base.py +48 -0
  36. repaykit/schedules/biweekly.py +18 -0
  37. repaykit/schedules/custom.py +42 -0
  38. repaykit/schedules/daily.py +28 -0
  39. repaykit/schedules/monthly.py +29 -0
  40. repaykit/schedules/quarterly.py +20 -0
  41. repaykit/schedules/weekly.py +23 -0
  42. repaykit/schedules/yearly.py +20 -0
  43. repaykit/terms/__init__.py +5 -0
  44. repaykit/terms/term.py +85 -0
  45. repaykit-1.0.0.dist-info/METADATA +285 -0
  46. repaykit-1.0.0.dist-info/RECORD +48 -0
  47. repaykit-1.0.0.dist-info/WHEEL +4 -0
  48. repaykit-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,48 @@
1
+ """Payment schedule interfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from collections.abc import Iterator
7
+ from datetime import date
8
+ from decimal import Decimal
9
+
10
+ from repaykit.exceptions import ScheduleGenerationError
11
+ from repaykit.terms import Term
12
+
13
+
14
+ class PaymentSchedule(ABC):
15
+ frequency_name: str
16
+ periods_per_year: Decimal
17
+
18
+ @abstractmethod
19
+ def _next_due_date(self, start_date: date, period: int) -> date:
20
+ """Return the due date for a 1-based period."""
21
+
22
+ def iter_due_dates(self, start_date: date, term: Term) -> Iterator[date]:
23
+ if term.periods is not None:
24
+ for period in range(1, term.periods + 1):
25
+ yield self._next_due_date(start_date, period)
26
+ return
27
+
28
+ end_date = term.end_after(start_date)
29
+ period = 1
30
+ while True:
31
+ due_date = self._next_due_date(start_date, period)
32
+ if due_date > end_date:
33
+ break
34
+ yield due_date
35
+ period += 1
36
+ if period > 1_000_000:
37
+ raise ScheduleGenerationError("schedule exceeded the safety period limit")
38
+
39
+ def period_count(self, start_date: date, term: Term) -> int:
40
+ count = sum(1 for _ in self.iter_due_dates(start_date, term))
41
+ if count <= 0:
42
+ raise ScheduleGenerationError("term and schedule must produce at least one period")
43
+ return count
44
+
45
+ def term_years(self, start_date: date, term: Term) -> Decimal:
46
+ if term.periods is not None:
47
+ return Decimal(term.periods) / self.periods_per_year
48
+ return term.approximate_years(start_date)
@@ -0,0 +1,18 @@
1
+ """Biweekly repayment schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import date, timedelta
7
+ from decimal import Decimal
8
+
9
+ from repaykit.schedules.base import PaymentSchedule
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class BiWeeklySchedule(PaymentSchedule):
14
+ frequency_name: str = "biweekly"
15
+ periods_per_year: Decimal = Decimal("26")
16
+
17
+ def _next_due_date(self, start_date: date, period: int) -> date:
18
+ return start_date + timedelta(weeks=period * 2)
@@ -0,0 +1,42 @@
1
+ """Custom explicit due-date schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator, Sequence
6
+ from dataclasses import dataclass
7
+ from datetime import date
8
+ from decimal import Decimal
9
+
10
+ from repaykit.exceptions import ValidationError
11
+ from repaykit.schedules.base import PaymentSchedule
12
+ from repaykit.terms import Term
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class CustomSchedule(PaymentSchedule):
17
+ dates: Sequence[date]
18
+ frequency_name: str = "custom"
19
+ periods_per_year: Decimal = Decimal("12")
20
+
21
+ def __post_init__(self) -> None:
22
+ if not self.dates:
23
+ raise ValidationError("custom schedule requires at least one date")
24
+ if list(self.dates) != sorted(self.dates):
25
+ raise ValidationError("custom schedule dates must be sorted")
26
+ if len(set(self.dates)) != len(self.dates):
27
+ raise ValidationError("custom schedule dates must be unique")
28
+
29
+ def _next_due_date(self, start_date: date, period: int) -> date:
30
+ return self.dates[period - 1]
31
+
32
+ def iter_due_dates(self, start_date: date, term: Term) -> Iterator[date]:
33
+ limit = term.periods if term.periods is not None else len(self.dates)
34
+ for due_date in self.dates[:limit]:
35
+ if due_date <= start_date:
36
+ raise ValidationError("custom schedule dates must be after start_date")
37
+ if term.end_date is not None and due_date > term.end_date:
38
+ break
39
+ yield due_date
40
+
41
+ def period_count(self, start_date: date, term: Term) -> int:
42
+ return sum(1 for _ in self.iter_due_dates(start_date, term))
@@ -0,0 +1,28 @@
1
+ """Daily repayment schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import date, timedelta
7
+ from decimal import Decimal
8
+
9
+ from repaykit.schedules.base import PaymentSchedule
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class DailySchedule(PaymentSchedule):
14
+ include_weekends: bool = True
15
+ frequency_name: str = "daily"
16
+ periods_per_year: Decimal = Decimal("365")
17
+
18
+ def _next_due_date(self, start_date: date, period: int) -> date:
19
+ if self.include_weekends:
20
+ return start_date + timedelta(days=period)
21
+
22
+ current = start_date
23
+ business_days = 0
24
+ while business_days < period:
25
+ current += timedelta(days=1)
26
+ if current.weekday() < 5:
27
+ business_days += 1
28
+ return current
@@ -0,0 +1,29 @@
1
+ """Monthly repayment schedules with month-end safety."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import calendar
6
+ from dataclasses import dataclass
7
+ from datetime import date
8
+ from decimal import Decimal
9
+
10
+ from repaykit.schedules.base import PaymentSchedule
11
+
12
+
13
+ def add_months(value: date, months: int, *, day: int | None = None) -> date:
14
+ month_index = value.month - 1 + months
15
+ year = value.year + month_index // 12
16
+ month = month_index % 12 + 1
17
+ target_day = value.day if day is None else day
18
+ last_day = calendar.monthrange(year, month)[1]
19
+ return date(year, month, min(target_day, last_day))
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class MonthlySchedule(PaymentSchedule):
24
+ day: int | None = None
25
+ frequency_name: str = "monthly"
26
+ periods_per_year: Decimal = Decimal("12")
27
+
28
+ def _next_due_date(self, start_date: date, period: int) -> date:
29
+ return add_months(start_date, period, day=self.day)
@@ -0,0 +1,20 @@
1
+ """Quarterly repayment schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import date
7
+ from decimal import Decimal
8
+
9
+ from repaykit.schedules.base import PaymentSchedule
10
+ from repaykit.schedules.monthly import add_months
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class QuarterlySchedule(PaymentSchedule):
15
+ day: int | None = None
16
+ frequency_name: str = "quarterly"
17
+ periods_per_year: Decimal = Decimal("4")
18
+
19
+ def _next_due_date(self, start_date: date, period: int) -> date:
20
+ return add_months(start_date, period * 3, day=self.day)
@@ -0,0 +1,23 @@
1
+ """Weekly repayment schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import date, timedelta
7
+ from decimal import Decimal
8
+
9
+ from repaykit.schedules.base import PaymentSchedule
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class WeeklySchedule(PaymentSchedule):
14
+ weekday: int | None = None
15
+ frequency_name: str = "weekly"
16
+ periods_per_year: Decimal = Decimal("52")
17
+
18
+ def _next_due_date(self, start_date: date, period: int) -> date:
19
+ if self.weekday is None:
20
+ return start_date + timedelta(weeks=period)
21
+ first = start_date + timedelta(days=1)
22
+ days_until = (self.weekday - first.weekday()) % 7
23
+ return first + timedelta(days=days_until + (period - 1) * 7)
@@ -0,0 +1,20 @@
1
+ """Yearly repayment schedules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import date
7
+ from decimal import Decimal
8
+
9
+ from repaykit.schedules.base import PaymentSchedule
10
+ from repaykit.schedules.monthly import add_months
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class YearlySchedule(PaymentSchedule):
15
+ day: int | None = None
16
+ frequency_name: str = "yearly"
17
+ periods_per_year: Decimal = Decimal("1")
18
+
19
+ def _next_due_date(self, start_date: date, period: int) -> date:
20
+ return add_months(start_date, period * 12, day=self.day)
@@ -0,0 +1,5 @@
1
+ """Term models."""
2
+
3
+ from repaykit.terms.term import Term
4
+
5
+ __all__ = ["Term"]
repaykit/terms/term.py ADDED
@@ -0,0 +1,85 @@
1
+ """Loan term modeling and parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from datetime import date, timedelta
8
+ from decimal import Decimal
9
+
10
+ from repaykit.exceptions import ValidationError
11
+
12
+ _TERM_RE = re.compile(
13
+ r"^\s*(?P<count>\d+)\s*"
14
+ r"(?P<unit>day|days|week|weeks|month|months|year|years|period|periods)\s*$"
15
+ )
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class Term:
20
+ days: int | None = None
21
+ weeks: int | None = None
22
+ months: int | None = None
23
+ years: int | None = None
24
+ periods: int | None = None
25
+ end_date: date | None = None
26
+
27
+ def __post_init__(self) -> None:
28
+ values = [self.days, self.weeks, self.months, self.years, self.periods, self.end_date]
29
+ if sum(value is not None for value in values) != 1:
30
+ raise ValidationError("exactly one term dimension must be provided")
31
+ for name in ("days", "weeks", "months", "years", "periods"):
32
+ value = getattr(self, name)
33
+ if value is not None and value <= 0:
34
+ raise ValidationError(f"term {name} must be positive")
35
+
36
+ @classmethod
37
+ def parse(cls, value: str | Term) -> Term:
38
+ if isinstance(value, Term):
39
+ return value
40
+ match = _TERM_RE.match(value)
41
+ if not match:
42
+ raise ValidationError("term must look like '24 months', '52 weeks', or '10 periods'")
43
+ count = int(match.group("count"))
44
+ unit = match.group("unit")
45
+ if unit.startswith("day"):
46
+ return cls(days=count)
47
+ if unit.startswith("week"):
48
+ return cls(weeks=count)
49
+ if unit.startswith("month"):
50
+ return cls(months=count)
51
+ if unit.startswith("year"):
52
+ return cls(years=count)
53
+ return cls(periods=count)
54
+
55
+ def approximate_years(self, start_date: date | None = None) -> Decimal:
56
+ if self.days is not None:
57
+ return Decimal(self.days) / Decimal("365")
58
+ if self.weeks is not None:
59
+ return Decimal(self.weeks) / Decimal("52")
60
+ if self.months is not None:
61
+ return Decimal(self.months) / Decimal("12")
62
+ if self.years is not None:
63
+ return Decimal(self.years)
64
+ if self.end_date is not None:
65
+ if start_date is None:
66
+ raise ValidationError("start_date is required for end_date terms")
67
+ return Decimal((self.end_date - start_date).days) / Decimal("365")
68
+ raise ValidationError("period-based terms need a schedule to infer years")
69
+
70
+ def end_after(self, start_date: date) -> date:
71
+ if self.days is not None:
72
+ return start_date + timedelta(days=self.days)
73
+ if self.weeks is not None:
74
+ return start_date + timedelta(weeks=self.weeks)
75
+ if self.months is not None:
76
+ from repaykit.schedules.monthly import add_months
77
+
78
+ return add_months(start_date, self.months)
79
+ if self.years is not None:
80
+ from repaykit.schedules.monthly import add_months
81
+
82
+ return add_months(start_date, self.years * 12)
83
+ if self.end_date is not None:
84
+ return self.end_date
85
+ raise ValidationError("period-based terms do not have a calendar end without a schedule")
@@ -0,0 +1,285 @@
1
+ Metadata-Version: 2.4
2
+ Name: repaykit
3
+ Version: 1.0.0
4
+ Summary: Pure Python repayment schedule, financing, loan ledger, and settlement calculation toolkit.
5
+ Project-URL: Homepage, https://github.com/finsure-techlab/repaykit
6
+ Project-URL: Repository, https://github.com/finsure-techlab/repaykit
7
+ Project-URL: Issues, https://github.com/finsure-techlab/repaykit/issues
8
+ Author: finsure techlab
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: amortization,financing,ledger,loan,repayment,settlement
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Office/Business :: Financial
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Provides-Extra: dev
25
+ Requires-Dist: build; extra == 'dev'
26
+ Requires-Dist: coverage; extra == 'dev'
27
+ Requires-Dist: mypy; extra == 'dev'
28
+ Requires-Dist: pytest; extra == 'dev'
29
+ Requires-Dist: pytest-cov; extra == 'dev'
30
+ Requires-Dist: ruff; extra == 'dev'
31
+ Requires-Dist: twine; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # repaykit
35
+
36
+ Pure Python repayment schedule, financing, loan ledger, and settlement calculation toolkit.
37
+
38
+ `repaykit` helps software teams calculate loan and financing schedules, payment ledgers, late
39
+ charges, statement balances, and settlement quotes. It is framework-agnostic, uses `Decimal` for
40
+ money and rates, and keeps business rules policy-driven.
41
+
42
+ ## What It Is Not
43
+
44
+ `repaykit` is not a loan origination system, accounting system, regulatory compliance engine, or
45
+ legal opinion. It does not provide legal, accounting, tax, Shariah, regulatory, lending, or
46
+ financial advice. Users are responsible for validating calculations, policies, disclosures, and
47
+ compliance requirements for their jurisdiction and product.
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install repaykit
53
+ ```
54
+
55
+ ## Supported Methods
56
+
57
+ - Flat rate
58
+ - Sum-of-digits / Rule of 78
59
+ - Reducing balance amortization
60
+ - Constant principal repayment
61
+
62
+ ## Supported Schedules
63
+
64
+ - Daily, with optional weekend skipping
65
+ - Weekly
66
+ - Biweekly
67
+ - Monthly, with month-end safety
68
+ - Quarterly
69
+ - Yearly
70
+ - Custom explicit due dates
71
+
72
+ ## Quickstart
73
+
74
+ ```python
75
+ from datetime import date
76
+ from decimal import Decimal
77
+
78
+ from repaykit import Loan
79
+
80
+ loan = Loan.create(
81
+ amount=Decimal("10000.00"),
82
+ annual_rate=Decimal("0.08"),
83
+ term="24 months",
84
+ payment_frequency="monthly",
85
+ method="flat_rate",
86
+ start_date=date(2026, 6, 1),
87
+ )
88
+
89
+ payment = loan.payment_amount()
90
+ rows = loan.schedule()
91
+ ```
92
+
93
+ Use `iter_schedule()` for lazy row generation, especially for daily schedules.
94
+
95
+ ## Expert Mode
96
+
97
+ ```python
98
+ from datetime import date
99
+ from decimal import Decimal
100
+
101
+ from repaykit import Loan
102
+ from repaykit.accruals import FlatAccrual
103
+ from repaykit.methods import FlatRateAllocation
104
+ from repaykit.policies import RoundingPolicy
105
+ from repaykit.schedules import MonthlySchedule
106
+ from repaykit.terms import Term
107
+
108
+ loan = Loan(
109
+ principal=Decimal("10000.00"),
110
+ term=Term(months=24),
111
+ payment_schedule=MonthlySchedule(day=1),
112
+ accrual_policy=FlatAccrual(annual_rate=Decimal("0.08")),
113
+ repayment_method=FlatRateAllocation(),
114
+ rounding=RoundingPolicy(currency="MYR"),
115
+ start_date=date(2026, 6, 1),
116
+ )
117
+ ```
118
+
119
+ ## Flat Rate Example
120
+
121
+ ```python
122
+ loan = Loan.create(
123
+ amount=Decimal("10000.00"),
124
+ annual_rate=Decimal("0.08"),
125
+ term="24 months",
126
+ payment_frequency="monthly",
127
+ method="flat_rate",
128
+ start_date=date(2026, 6, 1),
129
+ )
130
+ ```
131
+
132
+ Flat-rate total profit is `principal * annual_rate * term_in_years`. Future scheduled profit is
133
+ treated as unearned profit rebate in the default settlement policy.
134
+
135
+ ## Sum-of-Digits / Rule of 78 Example
136
+
137
+ ```python
138
+ loan = Loan.create(
139
+ amount=Decimal("10000.00"),
140
+ annual_rate=Decimal("0.08"),
141
+ term="12 months",
142
+ payment_frequency="monthly",
143
+ method="sum_of_digits",
144
+ start_date=date(2026, 1, 1),
145
+ )
146
+ ```
147
+
148
+ The denominator is generalized as `n * (n + 1) / 2`, so the method works with non-monthly period
149
+ counts such as 52 weekly periods.
150
+
151
+ ## Reducing Balance Example
152
+
153
+ ```python
154
+ loan = Loan.create(
155
+ amount=Decimal("10000.00"),
156
+ annual_rate=Decimal("0.08"),
157
+ term="24 months",
158
+ payment_frequency="monthly",
159
+ method="reducing_balance",
160
+ start_date=date(2026, 6, 1),
161
+ )
162
+ ```
163
+
164
+ Reducing balance uses fixed-payment amortization. Profit is calculated on the outstanding balance
165
+ for each period.
166
+
167
+ ## Constant Principal Example
168
+
169
+ ```python
170
+ loan = Loan.create(
171
+ amount=Decimal("10000.00"),
172
+ annual_rate=Decimal("0.08"),
173
+ term="24 months",
174
+ payment_frequency="monthly",
175
+ method="constant_principal",
176
+ start_date=date(2026, 6, 1),
177
+ )
178
+ ```
179
+
180
+ The principal component is constant except for the final rounding adjustment; installments decline
181
+ as profit declines.
182
+
183
+ ## Weekly Payment Example
184
+
185
+ ```python
186
+ loan = Loan.create(
187
+ amount=Decimal("5000.00"),
188
+ annual_rate=Decimal("0.10"),
189
+ term="52 weeks",
190
+ payment_frequency="weekly",
191
+ method="constant_principal",
192
+ start_date=date(2026, 1, 1),
193
+ )
194
+ ```
195
+
196
+ ## Daily Payment Example
197
+
198
+ ```python
199
+ loan = Loan.create(
200
+ amount=Decimal("1000.00"),
201
+ annual_rate=Decimal("0.12"),
202
+ term="30 days",
203
+ payment_frequency="daily",
204
+ method="reducing_balance",
205
+ start_date=date(2026, 1, 1),
206
+ )
207
+ ```
208
+
209
+ ## Payment Ledger Example
210
+
211
+ ```python
212
+ from repaykit import LoanAccount
213
+
214
+ account = LoanAccount(loan)
215
+ account.add_payment(
216
+ amount=Decimal("500.00"),
217
+ paid_at=date(2026, 7, 5),
218
+ reference="ANGKASA-JULY-2026",
219
+ )
220
+
221
+ statement = account.statement(as_of=date(2026, 8, 1))
222
+ print(statement.total_due)
223
+ print(statement.total_paid)
224
+ print(statement.arrears)
225
+ print(statement.outstanding_balance)
226
+ ```
227
+
228
+ Statements use aggregate oldest-due-first allocation for due installments and late-charge exposure.
229
+ They do not maintain a double-entry accounting ledger or split each payment into principal/profit
230
+ transactions.
231
+
232
+ ## Late Charge Example
233
+
234
+ ```python
235
+ from repaykit.policies import LateChargePolicy
236
+
237
+ loan.late_charge_policy = LateChargePolicy(
238
+ rate=Decimal("0.01"),
239
+ grace_days=5,
240
+ basis="flat",
241
+ minimum_charge=Decimal("0.00"),
242
+ )
243
+ ```
244
+
245
+ Supported bases are `flat` and `daily`. Negative overdue amounts are treated as zero.
246
+
247
+ ## Settlement Example
248
+
249
+ ```python
250
+ settlement = account.full_settlement(as_of=date(2026, 12, 15))
251
+
252
+ print(settlement.outstanding_principal)
253
+ print(settlement.unearned_profit_rebate)
254
+ print(settlement.late_charges)
255
+ print(settlement.amount_payable)
256
+ print(settlement.explanation)
257
+ ```
258
+
259
+ Settlement is policy-based. Flat-rate and sum-of-digits methods rebate future scheduled profit by
260
+ default. Reducing-balance and constant-principal methods have no unearned profit rebate by default.
261
+ Validate settlement behavior against your contract and regulatory requirements.
262
+
263
+ ## Export Example
264
+
265
+ ```python
266
+ from repaykit.exporters import schedule_to_csv, schedule_to_dicts
267
+
268
+ rows = loan.schedule()
269
+ data = schedule_to_dicts(rows)
270
+ schedule_to_csv(rows, "schedule.csv")
271
+ ```
272
+
273
+ Exporters serialize `Decimal` values as strings and dates as ISO strings. They never convert money
274
+ to floats.
275
+
276
+ ## Rounding Warning
277
+
278
+ The default `RoundingPolicy` uses half-up money rounding with two decimal places. Different
279
+ institutions and jurisdictions may require different rounding points, decimal places, or settlement
280
+ rebate rules. Configure and test policies against the governing contract and regulation.
281
+
282
+ ## Release Status
283
+
284
+ Version 1.0.0 declares the public API documented in `docs/api.md`. Breaking public API changes
285
+ after 1.0.0 require a major version bump.
@@ -0,0 +1,48 @@
1
+ repaykit/__init__.py,sha256=QJaAEmjbK5g4QHON4wScHZFuDuVjh2iEo0vGinE53nw,396
2
+ repaykit/_serialization.py,sha256=8r_QQAUe8GzLL0s6Rd5kP2CId6UE8-TQFBvEyk_iUS4,986
3
+ repaykit/account.py,sha256=yeO7jpEySMwq963OuxUWMY_V2W-jVtv4xTno_AXWITg,3610
4
+ repaykit/enums.py,sha256=DGotvaPfhdD-F1Ew_C9f55jYYoGndjuNkmltysye9mk,486
5
+ repaykit/exceptions.py,sha256=yJdh4zbrqF570Jy-j1FrDsLh6RM3WZVnkTyPiWIeabg,546
6
+ repaykit/loan.py,sha256=jBnvmK4pIIwzY09TQ8QwsVeVchhdMv2iHr-feUDwkCU,5532
7
+ repaykit/money.py,sha256=x9ioaQpJ986Xvirlo-2WlOdrdNNuX7o2Qd-bzpzqfLw,1055
8
+ repaykit/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
9
+ repaykit/results.py,sha256=dAAFQwxLTGyEnQ9tZHBnzwzoE6Mc9YIyk1hRD2griBQ,1396
10
+ repaykit/accruals/__init__.py,sha256=ljnWLm4xW-FZ6OV2l_IliYHvoeIQIRjps-uhbNRYQL8,304
11
+ repaykit/accruals/base.py,sha256=0dKEvwiuO_3Ly6CE8UwZaNXejt9oxzQMyWBpeq5Mmvg,829
12
+ repaykit/accruals/daily.py,sha256=ypiT2ozzE3363-f9ZGQACMGyVfNkqVJJ1nhtTsHRK80,517
13
+ repaykit/accruals/flat.py,sha256=Rxw9lGcBRSpFJFcFqOiH5ILzf5Nyn0W7cCWmjAn8Vx8,385
14
+ repaykit/accruals/periodic.py,sha256=79PMntB4gM3E1a9oLhry5iA3k3ubVPjygLghnaI_sHk,249
15
+ repaykit/backends/README.md,sha256=04oE8uy9xOphuhQWLlYRah9iwH0FvpeWDMvMQ3_vetc,576
16
+ repaykit/exporters/__init__.py,sha256=oQ-p5ey3KOomEGWOgMWo2Levi5Iztj8lttFfrFIfiHQ,298
17
+ repaykit/exporters/csv.py,sha256=8X2ZOA86nm3qiHAnQ0giZ8SxeoqRkxtZJm-XchOg6AY,829
18
+ repaykit/exporters/dicts.py,sha256=Xus6vhSNJ1y5Syd8tCcdfTWcTBgpyJanbIZ-hbjh-rE,414
19
+ repaykit/ledger/__init__.py,sha256=j4eTVRLN5BRHQV-Tlz_a11YlDxNVCKUgP-yN4uQXiwQ,234
20
+ repaykit/ledger/account_statement.py,sha256=Kut4diYKlC4I7bIk636n8zNSKLmvQyLylAXDGwKKIDg,90
21
+ repaykit/ledger/payment.py,sha256=EpIzCwIji2NyrIPmkUBtlV5Wmu74UzgDc26g0LsJMb8,763
22
+ repaykit/ledger/transaction.py,sha256=p7jh3HAnMdp9HjhkYPtl34vfUaGZSjWlquwzxukmbWY,395
23
+ repaykit/methods/__init__.py,sha256=8XDe6ulQkXFhMsE0XBgo_W4XWXd4f4DOdHvJrIOqOWQ,519
24
+ repaykit/methods/base.py,sha256=mFz2J8_tkO4T7KR_RBQuJuwZHbA-KRRtB7YZAtCGsGI,1232
25
+ repaykit/methods/constant_principal.py,sha256=LHGsWQJe3xiXTWa71uCfU7f2XDyvNibcDrS2nSXL19g,2694
26
+ repaykit/methods/flat_rate.py,sha256=g6ePHj6vXrDgf8MAI62X7fRwWhxqcsHd29bRf4MDLGU,3273
27
+ repaykit/methods/reducing_balance.py,sha256=FFl7P7_itUwMSPHajT2t-iuPTyEM-dcXDchdnTujlis,3031
28
+ repaykit/methods/sum_of_digits.py,sha256=lnwfjEBSFa34YgRCG89L927Cp45uJVNpFtRGJZrzq-Y,3776
29
+ repaykit/policies/__init__.py,sha256=9xNvFq2zaaVazYK93c0SFTRwocnYUrmJzQsdNwZ3POI,472
30
+ repaykit/policies/allocation.py,sha256=vEjbNFmUKmGLjvRaGlpJaiLmoDssLcAVTxuuN6UNoqk,470
31
+ repaykit/policies/late_charge.py,sha256=BjRY6qwI3_uS0KU_b5r6MXZo8macNJGsqc7ensrfSDM,2335
32
+ repaykit/policies/rounding.py,sha256=o3RvO3R-zScZYa646reTzDKkCIg2MxyTdE0d3tbyH88,1162
33
+ repaykit/policies/settlement.py,sha256=P59kTA8na3yN38SYlaZpszG3WgsvzIJAUvAgGH_2Qkk,2761
34
+ repaykit/schedules/__init__.py,sha256=-5MjkzSxjl3YzlBDPdlXAla4iy4y_JFH46b8rNHX2r0,666
35
+ repaykit/schedules/base.py,sha256=WGhEOBFugfoCsAATTeqxTKFZQed9QEKI_239CeRKA9M,1649
36
+ repaykit/schedules/biweekly.py,sha256=rveKnZWTQ2JzUtt0XJbDvbKRsnxTMh08ZrPmnnbLcTc,513
37
+ repaykit/schedules/custom.py,sha256=0UHfBCrRF_Mz2RtUA0hvmEhMn4Xwg99JuH-fOXQtruA,1618
38
+ repaykit/schedules/daily.py,sha256=M8W8_TmU0ObxLk1OeRdpIyBFaU5aK4iUHb9vN-Nj9wE,803
39
+ repaykit/schedules/monthly.py,sha256=mGSsa20idY6PL5L2yG_9AAjzREJ9-QLU2zHsrKoAiHo,923
40
+ repaykit/schedules/quarterly.py,sha256=5KM1OHwoZnExyiQqEJ0UeY52xFh-q24bImv3TuBPBo8,589
41
+ repaykit/schedules/weekly.py,sha256=ZOJNbQPlbzNP4JqSQq40iz4wwaLhf2iWl_NbbiEudAE,745
42
+ repaykit/schedules/yearly.py,sha256=TwhqKFQmAqTXZsSEf_fh1FDZsgIfsrdAvUtaDBgjfKk,581
43
+ repaykit/terms/__init__.py,sha256=OUHH5w461gkj2R7WC41QYuFLf8JcaACFAsXXmALsHTQ,77
44
+ repaykit/terms/term.py,sha256=DxV_sq-BrPoqCurM4CyPqhQcgj33vw8V8a80vq83XRg,3243
45
+ repaykit-1.0.0.dist-info/METADATA,sha256=poamC3RSXpbuqHi5tFYFGlXGgwMzaSznjU-nQRHzrxU,7821
46
+ repaykit-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
47
+ repaykit-1.0.0.dist-info/licenses/LICENSE,sha256=AeIppiqUtmEGOHqm3coSzkOOcT6XJipw4-BEcMfpdwk,1072
48
+ repaykit-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any