diff --git a/src/subscriptions/db.py b/src/subscriptions/db.py index 4d4c682..52bb94e 100644 --- a/src/subscriptions/db.py +++ b/src/subscriptions/db.py @@ -73,12 +73,32 @@ def get_by_customer(customer_handle: str) -> sqlite3.Row | None: ) +def freeze_votes_cursor(user_id: int) -> None: + """Réactivation après une période sans abonnement : repart de maintenant. + + Ne fait rien si le curseur est NULL (première activation jamais atteinte) : + les +2 initiaux restent gérés par credit_pending. Ne re-crédite jamais. + """ + row = get_by_user(user_id) + if row is None or row["votes_credited_until"] is None: + return + now = _now() + get_conn().execute( + "UPDATE subscriptions SET votes_credited_until = ?, updated_at = ? " + "WHERE user_id = ?", + (now, now, user_id), + ) + + def update_from_webhook( customer_handle: str, subscription_handle: str | None, status: str, current_period_end: str | None, ) -> None: + prev = get_by_customer(customer_handle) + if prev is not None and prev["status"] == "active" and status != "active": + credit_pending(prev["user_id"]) # banque les semaines acquises avant gel trial_flag = 1 if status in _ACCESS_STATUSES else 0 get_conn().execute( "UPDATE subscriptions SET " @@ -95,9 +115,14 @@ def update_from_webhook( customer_handle, ), ) + if prev is not None and prev["status"] != "active" and status == "active": + freeze_votes_cursor(prev["user_id"]) def set_cancelled(user_id: int, current_period_end: str | None) -> None: + credit_pending( + user_id + ) # banque les semaines pleines acquises (statut encore actif) get_conn().execute( "UPDATE subscriptions SET status = 'cancelled', current_period_end = ?, " "updated_at = ? WHERE user_id = ?", diff --git a/tests/subscriptions/test_db.py b/tests/subscriptions/test_db.py index c5f25d7..5bc87c3 100644 --- a/tests/subscriptions/test_db.py +++ b/tests/subscriptions/test_db.py @@ -191,3 +191,35 @@ def test_spend_vote_refused_when_balance_zero(users_db_path): _activate(uid, cursor_iso=None) # solde reste 0 tant que credit_pending n'est pas appelé assert db.spend_vote(uid) is False + + +def test_reactivation_resets_cursor_without_regranting(users_db_path): + db.init_schema() + uid = _make_user() + db.create_pending(uid, "decpinfo-1", "simple") + _activate(uid, cursor_iso=None) + db.credit_pending(uid) # +2, curseur posé + # désabonnement + db.update_from_webhook("decpinfo-1", "sub_1", "cancelled", _future()) + # période sans abonnement simulée : on recule artificiellement le curseur + old_cursor = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat() + db.get_conn().execute( + "UPDATE subscriptions SET votes_credited_until = ? WHERE user_id = ?", + (old_cursor, uid), + ) + # réabonnement + db.update_from_webhook("decpinfo-1", "sub_1", "active", _future()) + row = db.get_by_user(uid) + assert row["votes_balance"] == 2 # pas de re-crédit des +2 + # le curseur a été remis ~à maintenant → pas de crédit du gap de 30 jours + assert db.credit_pending(uid) == 2 + + +def test_trial_to_active_does_not_reset_then_grants_two(users_db_path): + db.init_schema() + uid = _make_user() + db.create_pending(uid, "decpinfo-1", "simple") + db.update_from_webhook("decpinfo-1", "sub_1", "trial", _future()) + db.update_from_webhook("decpinfo-1", "sub_1", "active", _future()) + # fin d'essai : credit_pending accorde les +2 initiaux + assert db.credit_pending(uid) == 2