use core::ops::Index;
use core::str::FromStr;
use beancount_importers_framework::error::ImporterBuilderError;
use beancount_importers_framework::error::UninitializedFieldSnafu;
use beancount_types::Account;
use beancount_types::AccountTemplate;
use beancount_types::Amount;
use beancount_types::Commodity;
use beancount_types::Directive;
use beancount_types::Link;
use beancount_types::MetadataKey;
use beancount_types::Seg;
use beancount_types::Transaction;
use derive_builder::Builder;
use miette::Diagnostic;
use miette::IntoDiagnostic as _;
use rust_decimal::Decimal;
use serde::Deserialize;
use snafu::Backtrace;
use snafu::OptionExt as _;
use snafu::Snafu;
use tap::Tap as _;
use time::Date;
#[derive(Debug, Diagnostic, Snafu)]
pub enum Error {}
#[derive(Builder, Clone, Debug, Deserialize)]
#[builder(
build_fn(error = "ImporterBuilderError", skip),
name = "ImporterBuilder",
setter(into),
try_setter
)]
pub struct Config {
pub base_account: AccountTemplate<TemplateSelector>,
pub expense_account: AccountTemplate<TemplateSelector>,
}
#[derive(Debug, Deserialize)]
pub struct Importer {
#[serde(flatten)]
pub(crate) config: Config,
#[serde(skip_deserializing)]
pub(crate) importer: csv::Importer,
}
impl Importer {
pub const NAME: &str = "amazon/digital-items";
pub fn new(config: Config) -> Self {
let importer = csv::Importer::default();
Self { config, importer }
}
pub fn builder() -> ImporterBuilder {
ImporterBuilder::default()
}
}
impl beancount_importers_framework::ImporterProtocol for Importer {
type Error = miette::Report;
fn account(&self, _buffer: &[u8]) -> Result<Account, Self::Error> {
Ok(self.config.base_account.base().to_owned())
}
fn date(&self, buffer: &[u8]) -> Option<Result<time::Date, Self::Error>> {
self.importer
.date(buffer, self)
.map(|result| result.map_err(Self::Error::from))
}
fn extract(
&self,
buffer: &[u8],
existing: &[Directive],
) -> Result<Vec<Directive>, Self::Error> {
self.importer
.extract(buffer, existing, self)
.map_err(Self::Error::from)
}
fn filename(&self, _buffer: &[u8]) -> Option<Result<String, Self::Error>> {
Some(Ok(String::from("digital-items.csv")))
}
fn identify(&self, buffer: &[u8]) -> Result<bool, Self::Error> {
const EXPECTED_HEADERS: &[&str] = &[
"ASIN",
"Title",
"OrderId",
"DigitalOrderItemId",
"DeclaredCountryCode",
"BaseCurrencyCode",
"FulfilledDate",
"IsFulfilled",
"Marketplace",
"OrderDate",
"OriginalQuantity",
"OurPrice",
"OurPriceCurrencyCode",
"OurPriceTax",
"OurPriceTaxCurrencyCode",
"SellerOfRecord",
"Publisher",
"ThirdPartyDisplayPrice",
"ThirdPartyDisplayCurrencyCode",
"ListPriceAmount",
"ListPriceCurrencyCode",
"ListPriceTaxAmount",
"ListPriceTaxCurrencyCode",
"GiftItem",
"OrderingCustomerNickname",
"GiftCustomerNickname",
"GiftMessage",
"GiftEmail",
"RecipientEmail",
"GiftRedemption",
"ItemMergedFromAnotherOrder",
"QuantityOrdered",
"ItemFulfilled",
"ShipFrom",
"ShipTo",
"IsOrderEligibleForPrimeBenefit",
"OfferingSKU",
"FulfillmentMobileNumber",
"RechargeAmount",
"RechargeAmountCurrencyCode",
"SubscriptionOrderInfoList",
"PreviouslyPaidDigitalOrderItemId",
"PreviouslyPaidOrderId",
"InstallmentOurPrice",
"InstallmentOurPricePlusTax",
"DigitalOrderItemAttributes",
"InstallmentOurPriceCurrencyCode",
"InstallmentOurPricePlusTaxCurrencyCode",
];
self.importer
.identify(buffer, EXPECTED_HEADERS)
.into_diagnostic()
}
fn name(&self) -> &'static str {
Self::NAME
}
#[doc(hidden)]
fn typetag_deserialize(&self) {}
}
impl csv::RecordImporter for Importer {
type Error = Error;
type Record<'de> = Record<'de>;
fn date(&self, record: Record) -> Date {
Date::max(
record.order_date,
record.fulfilled_date.unwrap_or(Date::MIN),
)
}
fn extract(
&self,
_existing: &[Directive],
record: Record,
) -> Result<Vec<Directive>, Self::Error> {
let date = record.order_date;
let asin = record.asin;
let product_name = record.title;
let order_id = record.order_id;
let amount = Amount::new(
record.our_price_tax.tap_mut(|amount| amount.rescale(2)),
record.our_price_tax_currency_code,
);
let transaction = Transaction::on(date).tap_mut(|transaction| {
let context = {
let currency: &str = &record.our_price_tax_currency_code;
let currency = <&Seg>::try_from(currency).expect("commodities are valid segments");
TemplateContext { currency }
};
transaction
.set_payee(record.seller_of_record)
.set_narration(product_name)
.add_link(Link::try_from(format!("^amazon.{order_id}")).expect("valid link"))
.add_meta(MetadataKey::try_from("order-id").unwrap(), order_id)
.add_meta(MetadataKey::try_from("asin").unwrap(), asin)
.build_posting(self.config.base_account.render(&context), |posting| {
posting.set_amount(-amount);
})
.build_posting(self.config.expense_account.render(&context), |_posting| {});
});
Ok(vec![Directive::from(transaction)])
}
}
impl ImporterBuilder {
pub fn build(&self) -> Result<Importer, ImporterBuilderError> {
let config = Config {
base_account: self.base_account.clone().context(UninitializedFieldSnafu {
field: "base_account",
importer: Importer::NAME,
})?,
expense_account: self
.expense_account
.clone()
.context(UninitializedFieldSnafu {
field: "expense_account",
importer: Importer::NAME,
})?,
};
Ok(Importer::new(config))
}
}
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Record<'r> {
#[serde(rename = "ASIN")]
asin: &'r str,
fulfilled_date: Option<Date>,
order_date: Date,
order_id: &'r str,
our_price_tax: Decimal,
our_price_tax_currency_code: Commodity,
seller_of_record: &'r str,
#[serde(rename = "Title")]
title: &'r str,
}
#[derive(Clone, Copy, Debug)]
pub struct TemplateContext<'c> {
pub(crate) currency: &'c Seg,
}
impl<'c> Index<&TemplateSelector> for TemplateContext<'c> {
type Output = Seg;
fn index(&self, selector: &TemplateSelector) -> &Self::Output {
match selector {
TemplateSelector::Currency => self.currency,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum TemplateSelector {
Currency,
}
impl FromStr for TemplateSelector {
type Err = TemplateSelectorError;
fn from_str(selector: &str) -> Result<Self, Self::Err> {
let selector = match selector {
"currency" => Self::Currency,
selector => return TemplateSelectorSnafu { selector }.fail(),
};
Ok(selector)
}
}
#[derive(Debug, Diagnostic, Snafu)]
#[snafu(display("unsupported context selector: {selector:?}"))]
pub struct TemplateSelectorError {
pub(crate) selector: String,
pub(crate) backtrace: Backtrace,
}