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
7 changes: 6 additions & 1 deletion ADMIN_API_ENDPOINTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2494,7 +2494,12 @@ Response:
"tax": 26250,
"company_id": 1,
"company_name": "Acme Corp",
"company_base_currency": "USD"
"company_base_currency": "USD",
"user_id": 456,
"host_id": 2,
"host_name": "host-01",
"region_id": 1,
"region_name": "US East"
}
]
}
Expand Down
4 changes: 4 additions & 0 deletions API_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]

### Added
- **2026-02-22** - Added additional fields to sales time-series report
- `GET /api/admin/v1/reports/time-series` — Response now includes `user_id`, `host_id`, `host_name`, `region_id`, `region_name` fields in each payment record
- Enables client-side filtering by user, host, or region

- **2026-02-21** - Added endpoint to list free IPs in an IPv4 range (Admin API)
- `GET /api/admin/v1/ip_ranges/{id}/free_ips` — Returns list of unassigned IP addresses
- Only available for IPv4 ranges; IPv6 ranges return an error (too large to enumerate)
Expand Down
13 changes: 13 additions & 0 deletions lnvps_api_admin/src/admin/reports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ struct TimeSeriesPayment {
company_id: u64,
company_name: String,
company_base_currency: String,
// User information
user_id: u64,
// Host information
host_id: u64,
host_name: String,
// Region information
region_id: u64,
region_name: String,
}

#[derive(Serialize, Deserialize)]
Expand Down Expand Up @@ -156,6 +164,11 @@ async fn admin_time_series_report(
company_id: payment.company_id,
company_name: payment.company_name.clone(),
company_base_currency: payment.company_base_currency.clone(),
user_id: payment.user_id,
host_id: payment.host_id,
host_name: payment.host_name.clone(),
region_id: payment.region_id,
region_name: payment.region_name.clone(),
});
}

Expand Down
5 changes: 5 additions & 0 deletions lnvps_api_common/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2093,6 +2093,11 @@ impl lnvps_db::AdminDb for MockDb {
company_id: region_company_id,
company_name: company.name.clone(),
company_base_currency: company.base_currency.clone(),
user_id: vm.user_id,
host_id: host.id,
host_name: host.name.clone(),
region_id: region.id,
region_name: region.name.clone(),
});
}
}
Expand Down
8 changes: 8 additions & 0 deletions lnvps_db/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,14 @@ pub struct VmPaymentWithCompany {
pub company_id: u64,
pub company_name: String,
pub company_base_currency: String,
// User information
pub user_id: u64,
// Host information
pub host_id: u64,
pub host_name: String,
// Region information
pub region_id: u64,
pub region_name: String,
}

#[derive(Type, Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
Expand Down
63 changes: 29 additions & 34 deletions lnvps_db/src/mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3165,41 +3165,36 @@ impl AdminDb for LNVpsDbMysql {
company_id: u64,
currency: Option<&str>,
) -> DbResult<Vec<VmPaymentWithCompany>> {
match currency {
Some(currency) => {
Ok(sqlx::query_as(
"SELECT vp.*, c.id as company_id, c.name as company_name, c.base_currency as company_base_currency
FROM vm_payment vp
JOIN vm v ON vp.vm_id = v.id
JOIN vm_host vh ON v.host_id = vh.id
JOIN vm_host_region vhr ON vh.region_id = vhr.id
JOIN company c ON vhr.company_id = c.id
WHERE vp.created >= ? AND vp.created < ? AND vp.is_paid = true AND c.id = ? AND vp.currency = ?
ORDER BY vp.created"
)
.bind(start_date)
.bind(end_date)
.bind(company_id)
.bind(currency)
.fetch_all(&self.db).await?)
},
None => {
Ok(sqlx::query_as(
"SELECT vp.*, c.id as company_id, c.name as company_name, c.base_currency as company_base_currency
FROM vm_payment vp
JOIN vm v ON vp.vm_id = v.id
JOIN vm_host vh ON v.host_id = vh.id
JOIN vm_host_region vhr ON vh.region_id = vhr.id
JOIN company c ON vhr.company_id = c.id
WHERE vp.created >= ? AND vp.created < ? AND vp.is_paid = true AND c.id = ?
ORDER BY vp.created"
)
.bind(start_date)
.bind(end_date)
.bind(company_id)
.fetch_all(&self.db).await?)
}
let mut query = QueryBuilder::new(
"SELECT vp.*,
c.id as company_id, c.name as company_name, c.base_currency as company_base_currency,
v.user_id,
vh.id as host_id, vh.name as host_name,
vhr.id as region_id, vhr.name as region_name
FROM vm_payment vp
JOIN vm v ON vp.vm_id = v.id
JOIN vm_host vh ON v.host_id = vh.id
JOIN vm_host_region vhr ON vh.region_id = vhr.id
JOIN company c ON vhr.company_id = c.id
WHERE vp.created >= ",
);
query.push_bind(start_date);
query.push(" AND vp.created < ");
query.push_bind(end_date);
query.push(" AND vp.is_paid = true AND c.id = ");
query.push_bind(company_id);

if let Some(currency) = currency {
query.push(" AND vp.currency = ");
query.push_bind(currency);
}

query.push(" ORDER BY vp.created");

Ok(query
.build_query_as::<VmPaymentWithCompany>()
.fetch_all(&self.db)
.await?)
}

async fn admin_get_referral_usage_by_date_range(
Expand Down
Loading