use std::{collections::HashMap, fmt::Display, net::IpAddr};
use bytesize::ByteSize;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use time::{OffsetDateTime, PrimitiveDateTime};
use time_tz::PrimitiveDateTimeExt;
use crate::{api::server::ServerId, urlencode::UrlEncode};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Product {
pub id: ProductId,
pub name: String,
pub description: Vec<String>,
#[serde(rename = "traffic", deserialize_with = "crate::conversion::traffic")]
pub traffic_limit: Option<ByteSize>,
#[serde(rename = "dist")]
pub distributions: Vec<String>,
#[serde(rename = "lang")]
pub languages: Vec<String>,
#[serde(default, rename = "location")]
pub locations: Vec<Location>,
#[serde(with = "location_prices")]
pub prices: HashMap<Location, LocationPrice>,
pub orderable_addons: Vec<Addon>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PurchasedProduct {
pub id: ProductId,
pub name: String,
pub description: Vec<String>,
#[serde(rename = "traffic", deserialize_with = "crate::conversion::traffic")]
pub traffic_limit: Option<ByteSize>,
#[serde(rename = "dist")]
pub distribution: String,
#[serde(rename = "lang")]
pub language: String,
#[serde(rename = "location")]
pub location: Option<Location>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PurchasedMarketProduct {
pub id: MarketProductId,
pub name: String,
pub description: Vec<String>,
#[serde(rename = "traffic", deserialize_with = "crate::conversion::traffic")]
pub traffic_limit: Option<ByteSize>,
#[serde(rename = "dist")]
pub distribution: String,
#[serde(rename = "lang")]
pub language: String,
#[serde(rename = "location")]
pub location: Option<Location>,
pub cpu: String,
pub cpu_benchmark: u32,
#[serde(deserialize_with = "crate::conversion::gb")]
pub memory_size: ByteSize,
#[serde(rename = "hdd_size", deserialize_with = "crate::conversion::gb")]
pub primary_hdd_size: ByteSize,
#[serde(rename = "hdd_text")]
pub features: String,
#[serde(rename = "hdd_count")]
pub primary_hdd_count: u8,
}
mod location_prices {
use super::*;
use serde::{Deserializer, Serializer};
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<HashMap<Location, LocationPrice>, D::Error> {
let prices = Vec::<SingleLocationPrice>::deserialize(deserializer)?;
Ok(prices
.into_iter()
.map(
|SingleLocationPrice {
location,
recurring: monthly,
setup,
}| {
(
location,
LocationPrice {
recurring: monthly,
setup,
},
)
},
)
.collect())
}
pub fn serialize<S>(
prices: &HashMap<Location, LocationPrice>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let prices: Vec<_> = prices
.iter()
.map(|(location, price)| SingleLocationPrice {
location: location.clone(),
recurring: price.recurring.clone(),
setup: price.setup.clone(),
})
.collect();
prices.serialize(serializer)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SingleLocationPrice {
pub location: Location,
#[serde(rename = "price")]
pub recurring: RecurringPrice,
#[serde(rename = "price_setup")]
pub setup: SetupPrice,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocationPrice {
pub recurring: RecurringPrice,
pub setup: SetupPrice,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RecurringPrice {
pub net: Decimal,
pub gross: Decimal,
pub hourly_net: Decimal,
pub hourly_gross: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SetupPrice {
pub net: Decimal,
pub gross: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Addon {
pub id: AddonId,
pub name: String,
pub location: Option<Location>,
pub min: u32,
pub max: u32,
#[serde(with = "location_prices")]
pub prices: HashMap<Location, LocationPrice>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvailableAddon {
pub id: AddonId,
pub name: String,
pub r#type: String,
pub price: SingleLocationPrice,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Location(pub String);
impl From<String> for Location {
fn from(value: String) -> Self {
Location(value)
}
}
impl From<&str> for Location {
fn from(value: &str) -> Self {
Location(value.to_string())
}
}
impl From<Location> for String {
fn from(value: Location) -> Self {
value.0
}
}
impl Display for Location {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for Location {
fn eq(&self, other: &str) -> bool {
self.0.eq(other)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Datacenter(pub String);
impl From<String> for Datacenter {
fn from(value: String) -> Self {
Datacenter(value)
}
}
impl From<&str> for Datacenter {
fn from(value: &str) -> Self {
Datacenter(value.to_string())
}
}
impl From<Datacenter> for String {
fn from(value: Datacenter) -> Self {
value.0
}
}
impl From<Datacenter> for Location {
fn from(value: Datacenter) -> Location {
Location(value.0.split_once('-').unwrap().0.to_string())
}
}
impl Display for Datacenter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for Datacenter {
fn eq(&self, other: &str) -> bool {
self.0.eq(other)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProductId(pub String);
impl From<String> for ProductId {
fn from(value: String) -> Self {
ProductId(value)
}
}
impl From<&str> for ProductId {
fn from(value: &str) -> Self {
ProductId(value.to_string())
}
}
impl From<ProductId> for String {
fn from(value: ProductId) -> Self {
value.0
}
}
impl Display for ProductId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for ProductId {
fn eq(&self, other: &str) -> bool {
self.0.eq(other)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductTransaction {
pub id: TransactionId,
#[serde(with = "time::serde::rfc3339")]
pub date: OffsetDateTime,
pub status: TransactionStatus,
#[serde(rename = "server_number")]
pub server_id: Option<ServerId>,
#[serde(
rename = "authorized_key",
deserialize_with = "crate::api::wrapper::deserialize_inner_vec"
)]
pub authorized_keys: Vec<InitialProductSshKey>,
#[serde(
rename = "host_key",
deserialize_with = "crate::api::wrapper::deserialize_inner_vec"
)]
pub host_keys: Vec<HostKey>,
pub comment: Option<String>,
pub product: PurchasedProduct,
pub addons: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TransactionStatus {
#[serde(rename = "ready")]
Ready,
#[serde(rename = "in process")]
InProcess,
#[serde(rename = "cancelled")]
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TransactionId(pub String);
impl From<String> for TransactionId {
fn from(value: String) -> Self {
TransactionId(value)
}
}
impl From<&str> for TransactionId {
fn from(value: &str) -> Self {
TransactionId(value.to_string())
}
}
impl From<TransactionId> for String {
fn from(value: TransactionId) -> Self {
value.0
}
}
impl Display for TransactionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for TransactionId {
fn eq(&self, other: &str) -> bool {
self.0.eq(other)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketTransaction {
pub id: MarketTransactionId,
#[serde(with = "time::serde::rfc3339")]
pub date: OffsetDateTime,
pub status: TransactionStatus,
#[serde(rename = "server_number")]
pub server_id: Option<ServerId>,
#[serde(
rename = "authorized_key",
deserialize_with = "crate::api::wrapper::deserialize_inner_vec"
)]
pub authorized_keys: Vec<InitialProductSshKey>,
#[serde(
rename = "host_key",
deserialize_with = "crate::api::wrapper::deserialize_inner_vec"
)]
pub host_keys: Vec<HostKey>,
pub comment: Option<String>,
pub product: PurchasedMarketProduct,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MarketTransactionId(pub String);
impl From<String> for MarketTransactionId {
fn from(value: String) -> Self {
MarketTransactionId(value)
}
}
impl From<&str> for MarketTransactionId {
fn from(value: &str) -> Self {
MarketTransactionId(value.to_string())
}
}
impl From<MarketTransactionId> for String {
fn from(value: MarketTransactionId) -> Self {
value.0
}
}
impl Display for MarketTransactionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for MarketTransactionId {
fn eq(&self, other: &str) -> bool {
self.0.eq(other)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AddonTransactionId(pub String);
impl From<String> for AddonTransactionId {
fn from(value: String) -> Self {
AddonTransactionId(value)
}
}
impl From<&str> for AddonTransactionId {
fn from(value: &str) -> Self {
AddonTransactionId(value.to_string())
}
}
impl From<AddonTransactionId> for String {
fn from(value: AddonTransactionId) -> Self {
value.0
}
}
impl Display for AddonTransactionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for AddonTransactionId {
fn eq(&self, other: &str) -> bool {
self.0.eq(other)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddonTransaction {
pub id: AddonTransactionId,
#[serde(with = "time::serde::rfc3339")]
pub date: OffsetDateTime,
pub status: TransactionStatus,
#[serde(rename = "server_number")]
pub server_id: ServerId,
pub product: PurchasedAddon,
pub resources: Vec<Resource>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resource {
pub r#type: String,
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PurchasedAddon {
pub id: AddonId,
pub name: String,
pub price: SingleLocationPrice,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AddonId(pub String);
impl From<String> for AddonId {
fn from(value: String) -> Self {
AddonId(value)
}
}
impl From<&str> for AddonId {
fn from(value: &str) -> Self {
AddonId(value.to_string())
}
}
impl From<AddonId> for String {
fn from(value: AddonId) -> Self {
value.0
}
}
impl Display for AddonId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for AddonId {
fn eq(&self, other: &str) -> bool {
self.0.eq(other)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct InitialProductSshKey {
pub name: String,
pub fingerprint: String,
#[serde(rename = "type")]
pub algorithm: String,
#[serde(rename = "size")]
pub bits: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct HostKey {
pub fingerprint: String,
#[serde(rename = "type")]
pub algorithm: String,
#[serde(rename = "size")]
pub bits: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct InternalMarketProduct {
pub id: MarketProductId,
pub name: String,
pub description: Vec<String>,
#[serde(rename = "traffic", deserialize_with = "crate::conversion::traffic")]
pub traffic_limit: Option<ByteSize>,
#[serde(rename = "dist")]
pub distributions: Vec<String>,
#[serde(rename = "lang")]
pub languages: Vec<String>,
pub datacenter: Option<String>,
pub cpu: String,
pub cpu_benchmark: u32,
#[serde(deserialize_with = "crate::conversion::gb")]
pub memory_size: ByteSize,
#[serde(deserialize_with = "crate::conversion::gb")]
pub hdd_size: ByteSize,
pub hdd_text: String,
pub hdd_count: u8,
pub price: Decimal,
pub price_vat: Decimal,
pub price_setup: Decimal,
pub price_hourly: Decimal,
pub price_hourly_vat: Decimal,
pub price_setup_vat: Decimal,
pub fixed_price: bool,
pub next_reduce: i64,
pub next_reduce_date: String,
pub orderable_addons: Vec<Addon>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "InternalMarketProduct")]
pub struct MarketProduct {
pub id: MarketProductId,
pub name: String,
pub description: Vec<String>,
pub traffic_limit: Option<ByteSize>,
pub distributions: Vec<String>,
pub languages: Vec<String>,
pub datacenter: Option<String>,
pub cpu: String,
pub cpu_benchmark: u32,
pub memory_size: ByteSize,
pub primary_hdd_size: ByteSize,
pub features: String,
pub primary_hdd_count: u8,
pub price: LocationPrice,
pub fixed_price: bool,
pub next_reduce_in: std::time::Duration,
pub next_reduce_at: Option<OffsetDateTime>,
pub orderable_addons: Vec<Addon>,
}
impl From<InternalMarketProduct> for MarketProduct {
fn from(value: InternalMarketProduct) -> Self {
MarketProduct {
id: value.id,
name: value.name,
description: value.description,
traffic_limit: value.traffic_limit,
distributions: value.distributions,
languages: value.languages,
datacenter: value.datacenter,
cpu: value.cpu,
cpu_benchmark: value.cpu_benchmark,
memory_size: value.memory_size,
primary_hdd_size: value.hdd_size,
features: value.hdd_text,
primary_hdd_count: value.hdd_count,
fixed_price: value.fixed_price,
price: LocationPrice {
recurring: RecurringPrice {
net: value.price,
gross: value.price_vat,
hourly_net: value.price_hourly,
hourly_gross: value.price_hourly_vat,
},
setup: SetupPrice {
net: value.price_setup,
gross: value.price_setup_vat,
},
},
next_reduce_in: std::time::Duration::from_secs(value.next_reduce.unsigned_abs()),
next_reduce_at: PrimitiveDateTime::parse(
&value.next_reduce_date,
&time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"),
)
.ok()
.and_then(|time| {
time.assume_timezone(time_tz::timezones::db::europe::BERLIN)
.take()
}),
orderable_addons: value.orderable_addons,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MarketProductId(pub u32);
impl From<u32> for MarketProductId {
fn from(value: u32) -> Self {
MarketProductId(value)
}
}
impl From<MarketProductId> for u32 {
fn from(value: MarketProductId) -> Self {
value.0
}
}
impl Display for MarketProductId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<u32> for MarketProductId {
fn eq(&self, other: &u32) -> bool {
self.0.eq(other)
}
}
#[derive(Debug, Clone)]
pub enum AuthorizationMethod {
Keys(Vec<String>),
Password(String),
}
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub enum ImSeriousAboutSpendingMoney {
LetMeSpendMyMoneyAlready,
#[default]
NoThisIsJustATest,
}
#[derive(Debug, Clone)]
pub struct ProductOrder {
pub id: ProductId,
pub auth: AuthorizationMethod,
pub location: Location,
pub distribution: Option<String>,
pub language: Option<String>,
pub comment: Option<String>,
pub addons: Vec<AddonId>,
pub i_want_to_spend_money_to_purchase_a_server: ImSeriousAboutSpendingMoney,
}
impl UrlEncode for ProductOrder {
fn encode_into(&self, mut f: crate::urlencode::UrlEncodingBuffer<'_>) {
f.set("product_id", &self.id);
match &self.auth {
AuthorizationMethod::Keys(keys) => {
for key in keys {
f.set("authorized_key[]", key)
}
}
AuthorizationMethod::Password(password) => {
f.set("password", password);
}
}
f.set("location", &self.location);
if let Some(dist) = &self.distribution {
f.set("dist", dist);
}
if let Some(lang) = &self.language {
f.set("lang", lang);
}
if let Some(comment) = &self.comment {
f.set("comment", comment);
}
for addon in &self.addons {
f.set("addon[]", addon);
}
if self.i_want_to_spend_money_to_purchase_a_server
== ImSeriousAboutSpendingMoney::LetMeSpendMyMoneyAlready
{
f.set("test", "false")
} else {
f.set("test", "true")
}
}
}
#[derive(Debug, Clone)]
pub struct MarketProductOrder {
pub id: MarketProductId,
pub auth: AuthorizationMethod,
pub distribution: Option<String>,
pub language: Option<String>,
pub comment: Option<String>,
pub addons: Vec<AddonId>,
pub i_want_to_spend_money_to_purchase_a_server: ImSeriousAboutSpendingMoney,
}
impl UrlEncode for MarketProductOrder {
fn encode_into(&self, mut f: crate::urlencode::UrlEncodingBuffer<'_>) {
f.set("product_id", self.id);
match &self.auth {
AuthorizationMethod::Keys(keys) => {
for key in keys {
f.set("authorized_key[]", key)
}
}
AuthorizationMethod::Password(password) => {
f.set("password", password);
}
}
if let Some(dist) = &self.distribution {
f.set("dist", dist);
}
if let Some(lang) = &self.language {
f.set("lang", lang);
}
if let Some(comment) = &self.comment {
f.set("comment", comment);
}
for addon in &self.addons {
f.set("addon[]", addon);
}
if self.i_want_to_spend_money_to_purchase_a_server
== ImSeriousAboutSpendingMoney::LetMeSpendMyMoneyAlready
{
f.set("test", "false")
} else {
f.set("test", "true")
}
}
}
#[derive(Debug, Clone)]
pub struct AddonOrder {
pub id: AddonId,
pub server: ServerId,
pub reason: Option<String>,
pub gateway: Option<IpAddr>,
pub i_want_to_spend_money_to_purchase_an_addon: ImSeriousAboutSpendingMoney,
}
impl UrlEncode for AddonOrder {
fn encode_into(&self, mut f: crate::urlencode::UrlEncodingBuffer<'_>) {
f.set("product_id", &self.id);
f.set("server_number", self.server);
if let Some(reason) = &self.reason {
f.set("reason", reason);
}
if let Some(gateway) = &self.gateway {
f.set("gateway", gateway);
}
if self.i_want_to_spend_money_to_purchase_an_addon
== ImSeriousAboutSpendingMoney::LetMeSpendMyMoneyAlready
{
f.set("test", "false")
} else {
f.set("test", "true")
}
}
}
#[cfg(test)]
mod tests {
use tracing::info;
use tracing_test::traced_test;
use crate::{
api::{
ordering::{
AddonId, AddonTransaction, AuthorizationMethod, AvailableAddon,
ImSeriousAboutSpendingMoney, MarketProductId, MarketTransaction,
ProductTransaction,
},
wrapper::List,
},
urlencode::UrlEncode,
};
use super::MarketProductOrder;
#[test]
#[traced_test]
fn test_serialize_market_product_order() {
let a = MarketProductOrder {
id: MarketProductId(100),
auth: AuthorizationMethod::Keys(vec![
String::from("15:28:b0:03:95:f0:77:b3:10:56:15:6b:77:22:a5:aa"),
String::from("15:28:b0:03:95:f0:77:b3:10:56:15:6b:77:22:a5:bb"),
]),
distribution: Some("Rescue System".to_string()),
language: Some("en".to_string()),
addons: vec![AddonId::from("primary_ipv4")],
comment: None,
i_want_to_spend_money_to_purchase_a_server:
ImSeriousAboutSpendingMoney::NoThisIsJustATest,
};
info!("{}", a.encode());
}
#[test]
#[traced_test]
fn deserialize_transactions() {
let example_data = r#"
[
{
"transaction":{
"id":"B20150121-344957-251478",
"date":"2015-01-21T12:30:43+01:00",
"status":"in process",
"server_number":null,
"server_ip":null,
"authorized_key":[
],
"host_key":[
],
"comment":null,
"product":{
"id":"VX6",
"name":"vServer VX6",
"description":[
"Single-Core CPU",
"1 GB RAM",
"25 GB HDD",
"No telephone support"
],
"traffic":"2 TB",
"dist":"Rescue system",
"@deprecated arch":"64",
"lang":"en",
"location":null
},
"addons":[
"primary_ipv4"
]
}
},
{
"transaction":{
"id":"B20150121-344958-251479",
"date":"2015-01-21T12:54:01+01:00",
"status":"ready",
"server_number":107239,
"server_ip":"188.40.1.1",
"authorized_key":[
{
"key":{
"name":"key1",
"fingerprint":"15:28:b0:03:95:f0:77:b3:10:56:15:6b:77:22:a5:bb",
"type":"ED25519",
"size":256
}
}
],
"host_key":[
{
"key":{
"fingerprint":"c1:e4:08:73:dd:f7:e9:d1:94:ab:e9:0f:28:b2:d2:ed",
"type":"DSA",
"size":1024
}
}
],
"comment":null,
"product":{
"id":"EX40",
"name":"Dedicated Root Server EX40",
"description":[
"Intel\u00ae Core\u2122 i7-4770 Quad-Core Haswell",
"32 GB DDR3 RAM",
"2 x 2 TB SATA 6 Gb\/s Enterprise HDD; 7200 rpm(Software-RAID 1)",
"1 Gbit\/s bandwidth"
],
"traffic":"30 TB",
"dist":"Debian 7.7 minimal",
"@deprecated arch":"64",
"lang":"en",
"location":"FSN1"
},
"addons":[
]
}
}
]"#;
let transactions: List<ProductTransaction> = serde_json::from_str(example_data).unwrap();
info!("{transactions:#?}");
}
#[test]
#[traced_test]
fn test_deserialize_market_transaction() {
let example_data = r#"
[
{
"transaction":{
"id":"B20150121-344957-251478",
"date":"2015-01-21T12:30:43+01:00",
"status":"in process",
"server_number":null,
"server_ip":null,
"authorized_key":[
],
"host_key":[
],
"comment":null,
"product":{
"id":283693,
"name":"SB110",
"description":[
"Intel Core i7 980x",
"6x RAM 4096 MB DDR3",
"2x HDD 1,5 TB SATA",
"2x SSD 120 GB SATA"
],
"traffic":"20 TB",
"dist":"Rescue system",
"@deprecated arch":"64",
"lang":"en",
"cpu":"Intel Core i7 980x",
"cpu_benchmark":8944,
"memory_size":24,
"hdd_size":1536,
"hdd_text":"ENT.HDD ECC INIC",
"hdd_count":2,
"datacenter":"FSN1-DC5",
"network_speed":"100 Mbit\/s",
"fixed_price":true,
"next_reduce":0,
"next_reduce_date":"2018-05-01 12:22:00"
}
}
},
{
"transaction":{
"id":"B20150121-344958-251479",
"date":"2015-01-21T12:54:01+01:00",
"status":"ready",
"server_number":107239,
"server_ip":"188.40.1.1",
"authorized_key":[
{
"key":{
"name":"key1",
"fingerprint":"15:28:b0:03:95:f0:77:b3:10:56:15:6b:77:22:a5:bb",
"type":"ED25519",
"size":256
}
}
],
"host_key":[
{
"key":{
"fingerprint":"c1:e4:08:73:dd:f7:e9:d1:94:ab:e9:0f:28:b2:d2:ed",
"type":"DSA",
"size":1024
}
}
],
"comment":null,
"product":{
"id":277254,
"name":"SB114",
"description":[
"Intel Core i7 950",
"6x RAM 2048 MB DDR3",
"7x HDD 1,5 TB SATA"
],
"traffic":"20 TB",
"dist":"Rescue system",
"@deprecated arch":"64",
"lang":"en",
"cpu":"Intel Core i7 950",
"cpu_benchmark":5682,
"memory_size":12,
"hdd_size":1536,
"hdd_text":"ENT.HDD ECC INIC",
"hdd_count":7,
"datacenter":"FSN1-DC5",
"network_speed":"100 Mbit\/s",
"fixed_price":true,
"next_reduce":0,
"next_reduce_date":"2018-05-01 12:22:00"
}
}
}
]"#;
let transactions: List<MarketTransaction> = serde_json::from_str(example_data).unwrap();
info!("{transactions:#?}");
}
#[test]
#[traced_test]
fn test_deserialize_addon_transactions() {
let example_data = r#"
[
{
"transaction":{
"id":"B20220210-1843193-S33055",
"date":"2022-02-10T12:20:11+01:00",
"status":"in process",
"server_number":123,
"product":{
"id":"failover_subnet_ipv4_29",
"name":"Failover subnet \/29",
"price":{
"location":"NBG1",
"price":{
"net":"15.1261",
"gross":"15.1261",
"hourly_net":"0.0242",
"hourly_gross":"0.0242"
},
"price_setup":{
"net":"152.0000",
"gross":"152.0000"
}
}
},
"resources":[
]
}
},
{
"transaction":{
"id":"B20220210-1843192-S33051",
"date":"2022-02-10T11:20:13+01:00",
"status":"ready",
"server_number":123,
"product":{
"id":"failover_subnet_ipv4_29",
"name":"Failover subnet \/29",
"price":{
"location":"NBG1",
"price":{
"net":"15.1261",
"gross":"15.1261",
"hourly_net":"0.0242",
"hourly_gross":"0.0242"
},
"price_setup":{
"net":"152.0000",
"gross":"152.0000"
}
}
},
"resources":[
{
"type":"subnet",
"id":"10.0.0.0"
}
]
}
}
]
"#;
let transactions: List<AddonTransaction> = serde_json::from_str(example_data).unwrap();
info!("{transactions:#?}");
}
#[test]
#[traced_test]
fn test_deserialize_available_addons() {
let example_data = r#"
[
{
"product":{
"id":"additional_ipv4",
"name":"Additional IP address",
"type":"ip_ipv4",
"price":{
"location":"NBG1",
"price":{
"net":"0.8403",
"gross":"0.8403",
"hourly_net":"0.0014",
"hourly_gross":"0.0014"
},
"price_setup":{
"net":"19.0000",
"gross":"19.0000"
}
}
}
},
{
"product":{
"id":"subnet_ipv4_29",
"name":"Additional subnet \/29 (monthly charge)",
"type":"subnet_ipv4",
"price":{
"location":"NBG1",
"price":{
"net":"6.7227",
"gross":"6.7227",
"hourly_net":"0.0108",
"hourly_gross":"0.0108"
},
"price_setup":{
"net":"152.0000",
"gross":"152.0000"
}
}
}
}
]"#;
let data: List<AvailableAddon> = serde_json::from_str(&example_data).unwrap();
info!("{data:#?}");
}
}