use crate::inventory::{
BarterItem, InventoryUpdate, Item, MoveItemRequest, RagfairResponseData, Upd,
};
use crate::{
handle_error, handle_error2, Error, ErrorResponse, Result, Tarkov, PROD_ENDPOINT,
TRADING_ENDPOINT,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, err_derive::Error)]
pub enum TradingError {
#[error(display = "transaction error")]
TransactionError,
#[error(display = "bad loyalty level")]
BadLoyaltyLevel,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct Trader {
#[serde(rename = "_id")]
pub id: String,
pub working: bool,
pub customization_seller: bool,
pub name: String,
pub surname: String,
pub nickname: String,
pub location: String,
pub avatar: String,
pub balance_rub: u64,
pub balance_dol: u64,
pub balance_eur: u64,
pub display: bool,
pub discount: i64,
pub discount_end: i64,
pub buyer_up: bool,
pub currency: Currency,
pub supply_next_time: u64,
pub repair: Repair,
pub insurance: Insurance,
#[serde(rename = "gridHeight")]
pub grid_height: u64,
pub loyalty: Loyalty,
pub sell_category: Vec<serde_json::Value>,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct Repair {
pub availability: bool,
pub quality: String,
pub excluded_id_list: Vec<String>,
pub excluded_category: Vec<String>,
pub currency: Option<String>,
pub currency_coefficient: Option<u64>,
pub price_rate: u64,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub enum Currency {
#[serde(rename = "RUB")]
Rouble,
#[serde(rename = "USD")]
Dollar,
#[serde(rename = "EUR")]
Euro,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct Insurance {
pub availability: bool,
pub min_payment: u64,
pub min_return_hour: u64,
pub max_return_hour: u64,
pub max_storage_time: u64,
pub excluded_category: Vec<String>,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Loyalty {
pub current_level: u64,
pub current_standing: f64,
pub current_sales_sum: u64,
pub loyalty_levels: HashMap<String, LoyaltyLevel>,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct LoyaltyLevel {
pub min_level: u64,
pub min_sales_sum: u64,
pub min_standing: f64,
}
#[derive(Debug, Deserialize)]
struct TradersResponse {
#[serde(flatten)]
error: ErrorResponse,
data: Option<Vec<Trader>>,
}
#[derive(Debug, Deserialize)]
struct TraderResponse {
#[serde(flatten)]
error: ErrorResponse,
data: Option<Trader>,
}
#[derive(Debug, Deserialize)]
struct TraderItemsResponse {
#[serde(flatten)]
error: ErrorResponse,
data: Option<TraderItems>,
}
#[derive(Debug, Deserialize)]
struct TraderItems {
items: Vec<Item>,
barter_scheme: HashMap<String, Vec<Vec<Price>>>,
loyal_level_items: HashMap<String, u8>,
}
#[derive(Debug, Deserialize)]
struct TraderPricesResponse {
#[serde(flatten)]
error: ErrorResponse,
data: Option<HashMap<String, Vec<Vec<Price>>>>,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct Price {
#[serde(rename = "_tpl")]
pub schema_id: String,
pub count: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TraderItem {
pub id: String,
pub schema_id: String,
pub upd: Option<Upd>,
pub price: Vec<Price>,
pub loyalty_level: u8,
}
#[derive(Debug, Serialize)]
struct TradeItemRequest<'a> {
#[serde(rename = "Action")]
action: &'a str,
#[serde(rename = "type")]
trade_type: &'a str,
#[serde(rename = "tid")]
trader_id: &'a str,
item_id: &'a str,
count: u64,
scheme_id: u64,
scheme_items: &'a [BarterItem],
}
#[derive(Debug, Deserialize)]
struct TradeResponse {
#[serde(flatten)]
error: ErrorResponse,
data: serde_json::Value,
}
#[derive(Debug, Serialize)]
struct SellItemRequest<'a> {
#[serde(rename = "Action")]
action: &'a str,
#[serde(rename = "type")]
trade_type: &'a str,
#[serde(rename = "tid")]
trader_id: &'a str,
items: &'a [SellItem],
}
#[derive(Debug, Serialize)]
struct SellItem {
id: String,
count: u64,
scheme_id: u64,
}
#[derive(Debug, Deserialize)]
struct SellResponse {
#[serde(flatten)]
error: ErrorResponse,
}
impl Tarkov {
pub async fn get_traders(&self) -> Result<Vec<Trader>> {
let url = format!("{}/client/trading/api/getTradersList", TRADING_ENDPOINT);
let res: TradersResponse = self.post_json(&url, &{}).await?;
handle_error(res.error, res.data)
}
pub async fn get_trader(&self, trader_id: &str) -> Result<Trader> {
if trader_id.is_empty() {
return Err(Error::InvalidParameters);
}
let url = format!(
"{}/client/trading/api/getTrader/{}",
TRADING_ENDPOINT, trader_id
);
let res: TraderResponse = self.post_json(&url, &{}).await?;
handle_error(res.error, res.data)
}
async fn get_trader_items_raw(&self, trader_id: &str) -> Result<TraderItems> {
let url = format!(
"{}/client/trading/api/getTraderAssort/{}",
TRADING_ENDPOINT, trader_id
);
let res: TraderItemsResponse = self.post_json(&url, &{}).await?;
handle_error(res.error, res.data)
}
async fn get_trader_prices_raw(
&self,
trader_id: &str,
) -> Result<HashMap<String, Vec<Vec<Price>>>> {
let url = format!(
"{}/client/trading/api/getUserAssortPrice/trader/{}",
TRADING_ENDPOINT, trader_id
);
let res: TraderPricesResponse = self.post_json(&url, &{}).await?;
handle_error(res.error, res.data)
}
pub async fn get_trader_items(&self, trader_id: &str) -> Result<Vec<TraderItem>> {
if trader_id.is_empty() {
return Err(Error::InvalidParameters);
}
let mut result: Vec<TraderItem> = Vec::new();
let items = self.get_trader_items_raw(trader_id).await?;
let prices = self.get_trader_prices_raw(trader_id).await?;
for item in items.items {
if item.parent_id != Some("hideout".to_string()) {
continue;
}
let loyalty_level = items
.loyal_level_items
.get(&item.id)
.expect("Loyalty level could not be mapped.");
let price = {
let barter_or_price = match items.barter_scheme.get(&item.id) {
None => prices
.get(&item.id)
.expect("Item price could not be mapped."),
Some(barter) => barter,
};
barter_or_price.get(0)
};
let trader_item = TraderItem {
id: item.id,
schema_id: item.schema_id,
upd: item.upd,
price: price.expect("Item price could not be mapped.").clone(),
loyalty_level: *loyalty_level,
};
result.push(trader_item);
}
Ok(result)
}
pub async fn trade_item(
&self,
trader_id: &str,
item_id: &str,
quantity: u64,
barter_items: &[BarterItem],
) -> Result<InventoryUpdate> {
if trader_id.is_empty() || item_id.is_empty() || quantity == 0 || barter_items.is_empty() {
return Err(Error::InvalidParameters);
}
let url = format!("{}/client/game/profile/items/moving", PROD_ENDPOINT);
let body = MoveItemRequest {
data: &[TradeItemRequest {
action: "TradingConfirm",
trade_type: "buy_from_trader",
trader_id,
item_id,
count: quantity,
scheme_id: 0,
scheme_items: barter_items,
}],
tm: 0,
};
let res: TradeResponse = self.post_json(&url, &body).await?;
handle_error2(res.error)?;
let res: RagfairResponseData = Deserialize::deserialize(res.data)?;
if !res.errors.is_empty() {
let error = &res.errors[0];
return Err(Error::UnknownAPIError(error.code));
}
let items: InventoryUpdate = Deserialize::deserialize(res.items)?;
Ok(items)
}
pub async fn sell_item(
&self,
trader_id: &str,
item_id: &str,
quantity: u64,
) -> Result<InventoryUpdate> {
if trader_id.is_empty() || item_id.is_empty() || quantity == 0 {
return Err(Error::InvalidParameters);
}
let url = format!("{}/client/game/profile/items/moving", PROD_ENDPOINT);
let body = MoveItemRequest {
data: &[SellItemRequest {
action: "TradingConfirm",
trade_type: "sell_to_trader",
trader_id,
items: &[SellItem {
id: item_id.to_string(),
count: quantity,
scheme_id: 0,
}],
}],
tm: 0,
};
let res: TradeResponse = self.post_json(&url, &body).await?;
handle_error2(res.error)?;
let res: RagfairResponseData = Deserialize::deserialize(res.data)?;
if !res.errors.is_empty() {
let error = &res.errors[0];
return Err(Error::UnknownAPIError(error.code));
}
let items: InventoryUpdate = Deserialize::deserialize(res.items)?;
Ok(items)
}
}