Referral program overhaul: commission, admin management, automated payouts#164
Merged
Conversation
Commission model — percentage of a referred VM's first payment: - company.referral_rate (default) + referral.referral_rate (per-referrer override, admin-controlled); effective rate = override ?? company default - earned amounts + admin referral report now report commission = amount * rate% - admin company create/update/get expose referral_rate Payout mode — replace referral.use_nwc bool with an extensible ReferralPayoutMode enum column (lightning_address | nwc | account_credit): - POST/PATCH/GET /api/v1/referral use 'mode' instead of 'use_nwc' - account_credit reserved for a future account-balance payout (rejected now) - migration maps use_nwc=1 -> nwc Includes migrations, model/mock/e2e updates, tests, docs + changelog. Part of work/referral-program-apis.md (PR1 of 4).
- Add AdminResource::Referral = 25 (Display/FromStr/TryFrom/all) + explicit
permission migration granting super_admin (per-feature convention)
- DB: admin_list_referrals (paginated; search by code substring or 64-char hex
pubkey via SQL HEX()), admin_get_referral; update_referral_payout also sets
invoice; mock impls + tests
- New admin/referrals.rs endpoints:
* GET /api/admin/v1/referrals list (paginated, search)
* GET /api/admin/v1/referrals/{id} detail (earned + payouts + counts)
* PATCH /api/admin/v1/referrals/{id} set/clear per-referrer commission override
* GET/POST /api/admin/v1/referrals/{id}/payouts list / create manual payout
* PATCH .../payouts/{payout_id} mark paid / reconcile (invoice, preimage)
- Responses never expose NWC secrets. Docs + changelog.
Part of work/referral-program-apis.md (PR2 of 4).
Dedicated ReferralPayoutHandler (lnvps_api/src/referral/mod.rs) — kept out of SubscriptionHandler; referrals are not a billing concern. - Accrual: owed BTC = commission on referred VMs' first payments minus existing BTC payouts (paid + reserved). Only BTC is auto-paid (Lightning settles sats); fiat commission accrues for manual admin payout. - Reserve-then-pay: insert an unpaid payout first, pay it, mark paid + record preimage; delete the reservation on failure so no double-pay and the balance can retry. Per-referrer errors don't abort the batch; referrer is notified. - Payout methods: LNURL-pay (lightning_address) or NWC make_invoice (nwc); node.pay_invoice settles it. account_credit rejected. - Opt-in via new 'referral' settings section (min-payout-sats, default 1000); WorkJob::ProcessReferralPayouts scheduled hourly only when configured. - DB base methods list_all_referrals / delete_referral_payout; update_referral_payout now also persists invoice. ApiReferralPayout exposes pre_image (hex). Enabled lnurl-rs async-https-native. Worker::new gained node. - Tests (payable math), config example, changelog. Part of work/referral-program-apis.md (PR3 of 4).
- DELETE /api/v1/referral: leave the program. Blocked (409) while a payout is pending, and when paid payout history exists (retained for accounting; the referral_payout FK has no cascade). New base DB method delete_referral. - GET /api/v1/referral/usage: per-referred-VM breakdown (vm_id, first payment amount, currency, effective_rate, commission). - Mock test for delete_referral/list_all_referrals; docs + changelog. Completes work/referral-program-apis.md (PR4 of 4).
The PATCH-time 'resulting config must be payable' check rejected clearing the lightning_address while in lightning_address mode, breaking a valid settings update (and the e2e lifecycle test). The payout worker already skips referrers whose method can't produce an invoice, so an incomplete config just defers payouts rather than losing them; signup still requires a valid method up-front.
The lifecycle test used the admin client (which targets the admin API server)
to assert a non-owner cannot PATCH a subscription. That server doesn't mount the
user route /api/v1/subscriptions/{id}, so it returned 404 (route not found)
instead of exercising the ownership check (403). Use a second user client (the
referrer) so the request hits the user API and the ownership guard.
Pre-existing bug from the auto-renewal PATCH change; surfaced now because e2e
only runs on PRs and this is the first e2e run since.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Completes the referral program end-to-end. The payout half was previously dead code (
insert/update_referral_payoutwere never called); commission had no configurable rate; and there were no admin management or user self-service endpoints. This adds all of it, delivered as four reviewable increments (seework/referral-program-apis.md).PR1 — Commission model + flexible payout mode (
4048398)referral.referral_rate, admin-set) falling back to a company default (company.referral_rate).use_nwcboolean with an extensibleReferralPayoutModeenum column (lightning_address|nwc|account_credit).POST/PATCH/GET /api/v1/referralusemode;account_creditis reserved/rejected. Migration mapsuse_nwc=1 → nwc.earnedand the admin referral report now report commission (amount * effective_rate%).PR2 — Admin management + RBAC (
4d19600)AdminResource::Referral = 25(explicit per-feature grant migration tosuper_admin).GET /api/admin/v1/referrals(paginated; search by code or 64-char hex pubkey),GET /api/admin/v1/referrals/{id}(earnings + payouts + counts),PATCH(set/clear the per-referrer override),GET/POST .../payouts(list / manual payout),PATCH .../payouts/{id}(mark paid / reconcile invoice + preimage). NWC secrets never exposed.PR3 — Automated payout worker (
8980e48)ReferralPayoutHandler(lnvps_api/src/referral/mod.rs) — kept out ofSubscriptionHandler.make_invoice, records preimage, notifies referrer. Only BTC is auto-paid; fiat left for manual payout.referralconfig section (min-payout-sats, default 1000);WorkJob::ProcessReferralPayoutsscheduled hourly only when configured.ApiReferralPayoutexposespre_image.PR4 — User self-service (
b5dac33)DELETE /api/v1/referral(leave; 409 while a payout is pending or paid history exists).GET /api/v1/referral/usage— per-referred-VM breakdown.Notes
referral_rate(company + referral),use_nwc → mode,ReferralRBAC permissions.cargo fmtcommit (32ee938).lnvps_db,lnvps_api,lnvps_api_commonincl.--features admin). Docs updated:ADMIN_API_ENDPOINTS.md,API_CHANGELOG.md.