diff --git a/views_api.py b/views_api.py index 5e008e0..e09ea00 100644 --- a/views_api.py +++ b/views_api.py @@ -14,6 +14,7 @@ from lnbits.decorators import check_super_user, check_user_exists from .crud import ( append_settlement_note, + count_completed_legs_for_settlement, create_dca_client, create_deposit, create_machine, @@ -573,7 +574,16 @@ async def api_retry_settlement( Voids any failed legs (completed legs are NEVER re-paid — Lightning sats already moved) and flips status 'errored' → 'pending', then re-invokes process_settlement. The optimistic-lock claim guards - against a concurrent listener re-fire racing this retry.""" + against a concurrent listener re-fire racing this retry. + + REFUSES when any leg has already completed. Reason: process_settlement + re-creates every leg from scratch (super_fee + operator_split + dca); + if a previous attempt already completed some of them, retrying would + DOUBLE-PAY those legs. For partial-success failures, the operator + needs to either edit the commission_splits ruleset to remove the + already-paid targets before retry, or manually pay the missing legs + out-of-band. + """ settlement = await get_settlement(settlement_id) if settlement is None: raise HTTPException(HTTPStatus.NOT_FOUND, "Settlement not found") @@ -586,6 +596,15 @@ async def api_retry_settlement( f"settlement status must be 'errored' to retry " f"(currently '{settlement.status}')", ) + completed = await count_completed_legs_for_settlement(settlement_id) + if completed > 0: + raise HTTPException( + HTTPStatus.BAD_REQUEST, + f"refusing to retry: {completed} leg(s) already completed. " + "Re-running distribution would double-pay them. Edit the " + "commission_splits ruleset to remove the already-paid targets, " + "or manually pay the missing legs.", + ) updated = await reset_settlement_for_retry(settlement_id) if updated is None or updated.status != "pending": raise HTTPException(