feat(v2): own dca_lp writes + auto-init on first dashboard access

Pairs with the satmachineadmin Phase 1 refactor that hoisted LP state
into `satoshimachine.dca_lp` (one row per user). This extension is now
the WRITER for that table; satmachineadmin only reads it during
distribution.

API surface:
  - `GET  /api/v1/dca-client/preferences` — returns the LP's
    `dca_lp` row, AUTO-CREATING it with the authenticated wallet
    as the default DCA destination on first call. Hitting this
    endpoint is the act that marks the LP as onboarded on the
    operator side (gating their deposit creation).
  - `PUT  /api/v1/dca-client/preferences` — LP-side update of
    wallet / mode / fixed-mode limit / autoforward fields. Ensures
    the row exists before applying. Replaces the old
    `PUT /autoforward` endpoint (which is gone).
  - `GET  /api/v1/dca-client/positions` — same shape as before
    but also auto-inits dca_lp on entry (so opening the dashboard
    onboards the LP). Now INNER JOINs dca_lp so only onboarded
    LPs see positions (matches the operator-side "must onboard
    before deposits" gate).
  - `GET  /api/v1/dca-client/transactions` — unchanged.

Models:
  - New `LpPreferences` / `UpdateLpPreferences` exposing the
    dca_lp fields.
  - `UpdateClientAutoforward` removed (replaced by the broader
    `UpdateLpPreferences`).
  - `PerMachinePosition.dca_mode` now sourced from `dca_lp` (it's
    LP-wide, echoed on each position row for legacy display
    compatibility).

CRUD:
  - `_fetch_user_clients` rewritten: INNER JOIN dca_lp, drop
    references to removed `dca_clients.wallet_id` / `.dca_mode`
    columns (they don't exist anymore post-Phase-1).
  - New: `get_lp_preferences`, `ensure_lp_preferences`,
    `update_lp_preferences`. The first writes nothing; the second
    is the get-or-create that defends the auto-onboard invariant.
  - `update_lp_autoforward` removed — write path is now
    `update_lp_preferences` against `dca_lp`, not the multi-row
    UPDATE on `dca_clients` that used to be needed because the
    state was denormalised across enrolments.

Note: the legacy static/js/index.js in this extension references
endpoints that no longer exist (`/registration-status`, `/register`,
`/dashboard/summary`, ...) — that's pre-existing tech debt from when
the LP UX was moved to ~/dev/webapp. Not regressed by this commit;
the deprecated frontend is out of scope. For now LP onboarding works
via direct API call (curl `GET /preferences` once with the LP's wallet
admin key); the webapp will own the proper UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-16 15:11:27 +02:00
commit 7dac898a10
3 changed files with 166 additions and 66 deletions

View file

@ -4,8 +4,14 @@
# wallet admin key (LP must be an LNbits user; the admin key identifies
# them as the wallet's owner).
#
# Maintenance-mode note: richer LP UI is moving to ~/dev/webapp. Keep this
# surface stable + minimal.
# This extension owns writes to `satoshimachine.dca_lp` — the LP's
# per-user preferences row (DCA wallet, mode, autoforward). Reads on
# any endpoint auto-init the row using the authenticated wallet as the
# default DCA destination, which is the act that satisfies the
# operator-side "must onboard before deposits accepted" gate.
#
# Maintenance-mode note: richer LP UI is moving to ~/dev/webapp. Keep
# this surface stable + minimal.
from http import HTTPStatus
from typing import List, Optional
@ -15,19 +21,54 @@ from lnbits.core.models import WalletTypeInfo
from lnbits.decorators import require_admin_key
from .crud import (
ensure_lp_preferences,
get_client_dashboard_summary,
get_client_transactions,
update_lp_autoforward,
update_lp_preferences,
)
from .models import (
ClientDashboardSummary,
ClientTransaction,
UpdateClientAutoforward,
LpPreferences,
UpdateLpPreferences,
)
satmachineclient_api_router = APIRouter()
@satmachineclient_api_router.get(
"/api/v1/dca-client/preferences", response_model=LpPreferences
)
async def api_get_preferences(
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> LpPreferences:
"""Return the LP's DCA preferences. Auto-creates the `dca_lp` row on
first call, seeded with the authenticated wallet as the default DCA
destination. The act of hitting this endpoint is what marks the LP
as "onboarded" on the operator side."""
return await ensure_lp_preferences(
wallet.wallet.user, default_wallet_id=wallet.wallet.id
)
@satmachineclient_api_router.put(
"/api/v1/dca-client/preferences", response_model=LpPreferences
)
async def api_update_preferences(
data: UpdateLpPreferences,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> LpPreferences:
"""LP-side update of DCA wallet / mode / autoforward. Operator can't
reach this path it requires the LP's wallet admin key."""
# Ensure the row exists before update so this endpoint is safe to
# call even if the LP somehow hits PUT before GET (they shouldn't,
# but the dashboard and any caller order shouldn't matter).
await ensure_lp_preferences(wallet.wallet.user, default_wallet_id=wallet.wallet.id)
updated = await update_lp_preferences(wallet.wallet.user, data)
assert updated is not None
return updated
@satmachineclient_api_router.get(
"/api/v1/dca-client/positions", response_model=ClientDashboardSummary
)
@ -35,8 +76,12 @@ async def api_get_positions(
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> ClientDashboardSummary:
"""LP's aggregated dashboard across every machine they're registered at,
plus a per-machine breakdown in the `positions` field."""
plus a per-machine breakdown in the `positions` field.
Also auto-creates the LP's `dca_lp` row on first access (so opening
the dashboard is itself the onboarding gesture)."""
user_id = wallet.wallet.user
await ensure_lp_preferences(user_id, default_wallet_id=wallet.wallet.id)
summary = await get_client_dashboard_summary(user_id)
if summary is None:
raise HTTPException(
@ -62,20 +107,3 @@ async def api_get_transactions(
return await get_client_transactions(
user_id, limit=limit, offset=offset, machine_id=machine_id
)
@satmachineclient_api_router.put(
"/api/v1/dca-client/autoforward", response_model=dict
)
async def api_update_autoforward(
data: UpdateClientAutoforward,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
"""LP controls their own auto-forward setting (where DCA distributions
get forwarded to externally, per satmachineadmin#8). Applies to all of
the LP's dca_clients rows; operator can't override LP-controlled
settings."""
user_id = wallet.wallet.user
n = await update_lp_autoforward(user_id, data)
return {"updated_clients": n}