Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 161 additions & 5 deletions ADMIN_API_ENDPOINTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1962,13 +1962,16 @@ Body:
// Optional - Postal/ZIP code
"phone": "string | null",
// Optional - Phone number
"email": "string | null"
"email": "string | null",
// Optional - Contact email
"referral_rate": 0.0
// Optional - Default referral commission (whole %, default 0) applied to a
// referred VM's first payment when the referrer has no per-referrer override
}
```

The `base_currency` field is validated against the supported Currency enum values. Invalid currency codes will be
rejected with an error message listing valid currencies.
rejected with an error message listing valid currencies. `referral_rate` must be >= 0.

#### Update Company

Expand Down Expand Up @@ -2002,12 +2005,16 @@ Body (all optional):
// Postal/ZIP code
"phone": "string | null",
// Phone number
"email": "string | null"
"email": "string | null",
// Contact email
"referral_rate": 0.0
// Default referral commission (whole %); must be >= 0
}
```

The `base_currency` field is validated against the supported Currency enum values.
The `base_currency` field is validated against the supported Currency enum values. `referral_rate` (when provided) must be >= 0.

GET/list company responses include `referral_rate` (the company's default referral commission %).

Note: Empty strings are treated as null values (clearing the field).

Expand Down Expand Up @@ -2997,6 +3004,151 @@ All messages are structured with a `type` field for consistent handling:
}
```

### Referral Program Management

All endpoints require the `referral` resource permissions. Responses never expose
NWC connection secrets (the NWC connection lives on the user's payment method).

#### List Referrals

```
GET /api/admin/v1/referrals
```

Required Permission: `referral::view`

Query Parameters:

- `limit`: number (optional, default 50, max 100)
- `offset`: number (optional, default 0)
- `search`: string (optional) - substring match on referral code, or a 64-char hex user pubkey

Returns a paginated list of `AdminReferralInfo`:

```json
{
"data": [
{
"id": 12,
"user_id": 34,
"user_pubkey": "<hex>",
"code": "ALPHA123",
"lightning_address": "user@domain.com",
"mode": "lightning_address",
"referral_rate": 12.5,
"created": "2026-07-18T10:00:00Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}
```

`referral_rate` is the per-referrer commission override (whole %); `null` means the referred VM's `company.referral_rate` default applies.

#### Get Referral Detail

```
GET /api/admin/v1/referrals/{id}
```

Required Permission: `referral::view`

Returns the referral plus per-currency earned commission, payout history and counts:

```json
{
"data": {
"id": 12,
"user_id": 34,
"user_pubkey": "<hex>",
"code": "ALPHA123",
"lightning_address": "user@domain.com",
"mode": "lightning_address",
"referral_rate": 12.5,
"created": "2026-07-18T10:00:00Z",
"earned": [ { "currency": "BTC", "amount": 5000 } ],
"payouts": [
{ "id": 1, "amount": 5000, "currency": "BTC", "created": "2026-07-18T11:00:00Z", "is_paid": true, "invoice": "lnbc...", "pre_image": "<hex>" }
],
"referrals_success": 3,
"referrals_failed": 1
}
}
```

`earned` is commission (`first payment * effective_rate%`) aggregated per currency.

#### Update Referral (commission override)

```
PATCH /api/admin/v1/referrals/{id}
```

Required Permission: `referral::update`

Body:

```json
{
"referral_rate": 12.5
// Set (number, >= 0), clear to company default (null), or omit to leave unchanged
}
```

#### List Referral Payouts

```
GET /api/admin/v1/referrals/{id}/payouts
```

Required Permission: `referral::view`

Returns an array of payout records (`AdminReferralPayoutInfo`), most recent first.

#### Create Referral Payout

```
POST /api/admin/v1/referrals/{id}/payouts
```

Required Permission: `referral::create`

Creates a manual payout record (e.g. reconciling an out-of-band payment).

```json
{
"amount": 5000,
// Required - smallest currency unit, > 0
"currency": "BTC",
// Required
"invoice": "lnbc...",
// Optional - associated Lightning invoice
"is_paid": false
// Optional - mark already paid (default false)
}
```

#### Update / Reconcile Referral Payout

```
PATCH /api/admin/v1/referrals/{id}/payouts/{payout_id}
```

Required Permission: `referral::update`

```json
{
"is_paid": true,
// Optional - mark paid/unpaid
"invoice": "lnbc...",
// Optional - set (string) or clear (null) the invoice
"pre_image": "<hex>"
// Optional - set (hex, 32 bytes) or clear (null) the payment preimage
}
```

### Reports

#### Time Series Report
Expand Down Expand Up @@ -3079,13 +3231,17 @@ Response:
"amount": 125000,
"currency": "USD",
"rate": 1.0,
"base_currency": "USD"
"base_currency": "USD",
"effective_rate": 10.0,
"commission": 12500
}
]
}
}
```

`amount` is the referred VM's first payment; `effective_rate` is the applied commission % (the referrer's per-referrer override if set, else the referred VM's `company.referral_rate`); `commission = amount * effective_rate%` (floored), in `currency` smallest units.

#### Profit/Loss Report

```
Expand Down
18 changes: 18 additions & 0 deletions API_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added

- **2026-07-18** - Referral program: leave + per-VM usage (user API)
- `DELETE /api/v1/referral` lets a referrer leave the program. Blocked (409) while a payout is pending, or when paid payout history exists (retained for accounting).
- `GET /api/v1/referral/usage` returns a per-referred-VM breakdown: `vm_id`, `created`, `amount` (the VM's first payment), `currency`, `effective_rate`, and `commission`.

- **2026-07-18** - Automated referral commission payouts
- A new opt-in worker job pays referrers their accrued **BTC** commission over Lightning. For each referrer whose owed commission (earned minus already paid/reserved) clears a configurable threshold, it reserves a payout, fetches a BOLT11 invoice (LNURL-pay for a `lightning_address` referrer, or NWC `make_invoice` for an `nwc` referrer), pays it from the node, and records the preimage. Non-BTC (fiat) commission is not auto-paid and is left for manual admin payout. Enabled by adding a `referral` config section (`min-payout-sats`, default 1000); when omitted, automated payouts are disabled. `GET /api/v1/referral` payout records now include `pre_image` (hex, once settled).

- **2026-07-18** - Admin referral program management
- New admin endpoints under the `referral` RBAC resource (granted to `super_admin`): `GET /api/admin/v1/referrals` (paginated; `search` by code substring or 64-char hex pubkey), `GET /api/admin/v1/referrals/{id}` (referral + per-currency earned commission + payout history + success/failed counts), `PATCH /api/admin/v1/referrals/{id}` (set/clear the per-referrer commission override), `GET`/`POST /api/admin/v1/referrals/{id}/payouts` (list / create a manual payout record), and `PATCH /api/admin/v1/referrals/{id}/payouts/{payout_id}` (mark paid / set invoice / set preimage for reconciliation). NWC secrets are never exposed.

- **2026-07-18** - Flexible referral payout mode (replaces `use_nwc`)
- The referral payout method is now an extensible `mode` enum instead of the `use_nwc` boolean. `GET /api/v1/referral` returns `mode` (`lightning_address` | `nwc` | `account_credit`) instead of `use_nwc`. `POST`/`PATCH /api/v1/referral` accept `mode` instead of `use_nwc`: `lightning_address` (default) requires a resolvable `lightning_address`; `nwc` requires a configured NWC connection; `account_credit` is reserved for a future account-balance payout and is currently rejected. Existing enrollments migrate `use_nwc = true` → `nwc`, otherwise `lightning_address`.

- **2026-07-18** - Referral commission rate (percentage of first payment)
- The referral program now pays a commission = a percentage of each referred VM's **first** payment. The effective rate is per-referrer with a company default: `company.referral_rate` (new, default `0`) applies unless the referrer has an override. Admin company `POST`/`PATCH /api/admin/v1/companies` accept `referral_rate` (whole %, `>= 0`) and GET/list responses expose it.
- `GET /api/v1/referral` now returns `referral_rate` (the per-referrer override, `null` = use company default; read-only — set by admins) and its `earned` amounts are now the commission (`payment * effective_rate%`) rather than the full first payment.
- `GET /api/admin/v1/reports/referral-usage/time-series` rows gain `effective_rate` and `commission`.

- **2026-07-18** - OSS (One-Stop Shop) VAT report
- `GET /api/admin/v1/reports/oss` aggregates cross-border EU B2C sales (`tax_treatment = oss_b2c`) by filing period and destination member state for transcription onto an OSS VAT return. Query params: `start_date`, `end_date` (YYYY-MM-DD), optional `company_id` (`0`/omitted = all), and `period` = `quarter` (default, calendar Q1-Q4) | `bimonthly` (two-month buckets B1-B6). Rows are keyed by `(period, company, destination country, VAT rate)` and expressed in each seller company's base currency using the exchange rate frozen on each payment. Only paid payments are counted. Requires `analytics::view`.

Expand Down
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lnvps_api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ rand = "0.9"
clap = { version = "4.5", features = ["derive"] }
ssh-key = "0.6"
lettre = { version = "0.11", features = ["tokio1-native-tls"] }
lnurl-rs = { version = "0.9", default-features = false }
lnurl-rs = { version = "0.9", default-features = false, features = ["async-https-native"] }
mustache = "0.9"
isocountry = "0.3"
hickory-resolver = "0.26"
Expand Down
5 changes: 5 additions & 0 deletions lnvps_api/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ lightning:
cert: "/home/kieran/.polar/networks/1/volumes/lnd/alice/tls.cert"
macaroon: "/home/kieran/.polar/networks/1/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon"
delete-after: 3
# Automated referral commission payouts (opt-in). Omit this section to disable
# automatic payouts (commission still accrues and can be paid manually by admins).
referral:
# Minimum accrued BTC commission (satoshis) before a payout is attempted.
min-payout-sats: 1000
public-url: "https://api.lnvps.net"
read-only: false
redis:
Expand Down
5 changes: 4 additions & 1 deletion lnvps_api/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ pub(crate) async fn resolve_payment_mode(
"No NWC payment method configured"
)));
}
Ok((PaymentMethod::Lightning, RenewMode::Saved { method_id: None }))
Ok((
PaymentMethod::Lightning,
RenewMode::Saved { method_id: None },
))
}
Some("saved") => Ok((
PaymentMethod::Revolut,
Expand Down
Loading
Loading