feat: Zahlungen finden ihren Vertrag von selbst – Preisaenderungen werden gemeldet
Ein Abo stand bisher unverbunden neben seinen Zahlungen. Wer wissen wollte, ob Netflix diesen Monat abgebucht wurde, musste es in der Buchungsliste suchen. Jetzt verknuepft Certain Buchung und Vertrag beim Import selbst. Drei Stufen, bewusst unterschiedlich mutig: - sicher (Betrag und Text passen, Datum im Fenster): wird zugeordnet. - Vorschlag: Betrag passt, Text nicht – oder der Text passt und der Betrag weicht ab. Der zweite Fall ist die Preiserhoehung. - nichts: im Zweifel lieber schweigen. Eine Preiserhoehung wird nie stillschweigend uebernommen. Sie ist eine Nachricht an den Nutzer, kein Wartungsvorgang – er entscheidet, ob der neue Preis der richtige ist. Die Zuordnung ist ein Etikett, keine Umbuchung: die Buchung selbst bleibt unangetastet, und ein geloeschter Vertrag nimmt sie nicht mit. Beim Backup wandern Vertraege nun vor die Buchungen, sonst haenge eine Rueckspielung Zahlungen an die falschen Vertraege. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
41db67f53e
commit
152483707e
@@ -23,6 +23,7 @@ from ..models import (
|
||||
CategoryKind,
|
||||
Subscription,
|
||||
SubscriptionLogo,
|
||||
Transaction,
|
||||
User,
|
||||
)
|
||||
from ..schemas import (
|
||||
@@ -32,7 +33,7 @@ from ..schemas import (
|
||||
SubscriptionSummary,
|
||||
SubscriptionUpdate,
|
||||
)
|
||||
from ..services import currency, logo as logo_service
|
||||
from ..services import abo_erkennung, currency, logo as logo_service
|
||||
from .settings import BASE_CURRENCY_KEY, get_user_setting
|
||||
|
||||
router = APIRouter(prefix="/subscriptions", tags=["subscriptions"])
|
||||
@@ -154,6 +155,89 @@ def create_subscription(payload: SubscriptionIn, db: Session = Depends(get_db),
|
||||
return to_out(db, s, _base(db, user), date.today())
|
||||
|
||||
|
||||
# --- Zuordnung: welche Buchung gehoert zu welchem Vertrag --------------------
|
||||
|
||||
|
||||
@router.post("/zuordnen")
|
||||
def zuordnen(db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user)) -> dict:
|
||||
"""Buchungen ihren Vertraegen zuordnen.
|
||||
|
||||
Eindeutig Sicheres wird zugeordnet, der Rest kommt als Vorschlag zurueck.
|
||||
Laeuft ueber ALLE noch nicht zugeordneten Buchungen, auch alte – wer seine
|
||||
Vertraege nachtraegt, bekommt damit rueckwirkend seinen Verlauf.
|
||||
"""
|
||||
return abo_erkennung.finde(db, user)
|
||||
|
||||
|
||||
@router.get("/vorschlaege")
|
||||
def vorschlaege(db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user)) -> dict:
|
||||
"""Nur fragen, nichts veraendern – fuer eine Ansicht, die bloss hinsieht."""
|
||||
return abo_erkennung.finde(db, user, nur_vorschlaege=True)
|
||||
|
||||
|
||||
@router.post("/{sub_id}/buchungen/{txn_id}", response_model=SubscriptionOut)
|
||||
def zuordnen_von_hand(sub_id: int, txn_id: int, db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user)):
|
||||
"""Eine Buchung diesem Vertrag zuordnen – einen Vorschlag annehmen."""
|
||||
s = get_owned(db, Subscription, sub_id, user)
|
||||
txn = get_owned(db, Transaction, txn_id, user)
|
||||
txn.subscription_id = s.id
|
||||
if txn.date is not None:
|
||||
abo_erkennung._ruecke_faelligkeit(s, txn.date)
|
||||
db.commit()
|
||||
db.refresh(s)
|
||||
return to_out(db, s, _base(db, user), date.today())
|
||||
|
||||
|
||||
@router.delete("/buchungen/{txn_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def zuordnung_loesen(txn_id: int, db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user)) -> None:
|
||||
"""Das Etikett abziehen. Die Buchung bleibt, wie sie ist."""
|
||||
txn = get_owned(db, Transaction, txn_id, user)
|
||||
txn.subscription_id = None
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.get("/{sub_id}/verlauf")
|
||||
def verlauf(sub_id: int, db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user)) -> dict:
|
||||
"""Die erkannten Abbuchungen dieses Vertrags.
|
||||
|
||||
Beantwortet "was habe ich dafuer insgesamt bezahlt" – und zeigt, wann der
|
||||
Preis gestiegen ist.
|
||||
"""
|
||||
s = get_owned(db, Subscription, sub_id, user)
|
||||
zeilen = abo_erkennung.verlauf(db, user, s)
|
||||
return {
|
||||
"buchungen": zeilen,
|
||||
"anzahl": len(zeilen),
|
||||
"summe_cents": sum(z["amount_cents"] for z in zeilen),
|
||||
"currency": zeilen[0]["currency"] if zeilen else s.currency,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{sub_id}/preis", response_model=SubscriptionOut)
|
||||
def preis_uebernehmen(sub_id: int, amount_cents: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user)):
|
||||
"""Einen geaenderten Preis uebernehmen.
|
||||
|
||||
Eigener Schritt und nicht automatisch: Eine Preiserhoehung ist eine
|
||||
Nachricht an den Nutzer, kein Wartungsvorgang. Wer sie stillschweigend
|
||||
nachzieht, nimmt ihm die einzige Gelegenheit, sie zu bemerken.
|
||||
"""
|
||||
if amount_cents < 0:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Betrag darf nicht negativ sein")
|
||||
s = get_owned(db, Subscription, sub_id, user)
|
||||
s.amount_cents = amount_cents
|
||||
db.commit()
|
||||
db.refresh(s)
|
||||
return to_out(db, s, _base(db, user), date.today())
|
||||
|
||||
|
||||
@router.get("/{sub_id}", response_model=SubscriptionOut)
|
||||
def get_subscription(sub_id: int, db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user)):
|
||||
|
||||
Reference in New Issue
Block a user