mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-25 15:05:18 +02:00
feat: add SEPA direct debit exports and notifications
This commit is contained in:
@@ -983,6 +983,84 @@ class MemberRepository:
|
||||
)
|
||||
return payment
|
||||
|
||||
def record_combined_payment(
|
||||
self,
|
||||
member_id: str,
|
||||
*,
|
||||
payment_date: str,
|
||||
allocations: dict[str, str],
|
||||
reference: str = "",
|
||||
method: str = "bank_transfer",
|
||||
actor_name: str = "CCMA",
|
||||
) -> dict:
|
||||
"""Record one payment and atomically allocate it to multiple claims."""
|
||||
try:
|
||||
normalized_date = normalize_date_input(payment_date, "Zahlungsdatum")
|
||||
except DateValidationError as exc:
|
||||
raise RepositoryError(str(exc)) from exc
|
||||
if not normalized_date:
|
||||
raise RepositoryError("Zahlungsdatum ist erforderlich.")
|
||||
if not allocations:
|
||||
raise RepositoryError("Mindestens eine Zuordnung ist erforderlich.")
|
||||
|
||||
data = self.get_contributions(member_id)
|
||||
claims_by_id = {str(claim.get("claim_id", "")): claim for claim in data.claims}
|
||||
selected_allocations: dict[str, Decimal] = {}
|
||||
for claim_id, raw_amount in allocations.items():
|
||||
claim = claims_by_id.get(claim_id)
|
||||
if claim is None:
|
||||
raise RepositoryError(f"Forderung nicht gefunden: {claim_id}")
|
||||
if str(claim.get("status", "")) == "cancelled":
|
||||
raise RepositoryError("Eine stornierte Forderung kann nicht bezahlt werden.")
|
||||
try:
|
||||
selected_amount = decimal_value(raw_amount, "Zuordnung")
|
||||
except ValueError as exc:
|
||||
raise RepositoryError(str(exc)) from exc
|
||||
available = max(claim_balance(data, claim), Decimal("0"))
|
||||
if selected_amount <= 0 or selected_amount > available:
|
||||
raise RepositoryError(
|
||||
f"{claim.get('title', 'Forderung')} hat nur "
|
||||
f"{money_text(available)} EUR offen."
|
||||
)
|
||||
selected_allocations[claim_id] = selected_amount
|
||||
|
||||
total = sum(selected_allocations.values(), Decimal("0"))
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
payment = {
|
||||
"payment_id": str(uuid4()),
|
||||
"date": normalized_date,
|
||||
"amount": money_text(total),
|
||||
"method": method.strip() or "bank_transfer",
|
||||
"gnucash_transaction_id": "",
|
||||
"reference": reference.strip(),
|
||||
"created_at": now,
|
||||
}
|
||||
for claim_id, amount in selected_allocations.items():
|
||||
data.allocations.append(
|
||||
{
|
||||
"allocation_id": str(uuid4()),
|
||||
"payment_id": payment["payment_id"],
|
||||
"claim_id": claim_id,
|
||||
"amount": money_text(amount),
|
||||
}
|
||||
)
|
||||
data.payments.append(payment)
|
||||
self.save_contributions(member_id, data)
|
||||
self.append_event(
|
||||
member_id,
|
||||
event_type="payment_recorded",
|
||||
summary=f"Zahlung eingegangen: {payment['amount']} EUR",
|
||||
actor_type="system" if method == "dummy" else "user",
|
||||
actor_name=actor_name,
|
||||
references={"payment_id": str(payment["payment_id"])},
|
||||
data={
|
||||
"allocated_amount": payment["amount"],
|
||||
"claim_ids": list(selected_allocations),
|
||||
"method": payment["method"],
|
||||
},
|
||||
)
|
||||
return payment
|
||||
|
||||
def allocate_payment(self, member_id: str, claim_id: str, *, payment_id: str, amount: str) -> dict:
|
||||
data, claim = self.get_claim(member_id, claim_id)
|
||||
payment = next(
|
||||
|
||||
Reference in New Issue
Block a user