KB7Y4PJIETGK43QVN5JTCQ2UD3JCXKYDULKTNQVR7WMH2JIODS7AC
use core::ops::Index;
use core::str::FromStr;
use beancount_importers_framework::error::ImporterBuilderError;
use beancount_importers_framework::error::UninitializedFieldSnafu;
use beancount_types::common_keys;
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 camino::Utf8Path;
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::format_description::well_known::Rfc3339;
use time::macros::format_description;
use time::OffsetDateTime;
use time::PrimitiveDateTime;
use time_tz::OffsetDateTimeExt as _;
#[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/order-history";
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, _file: &Utf8Path) -> Result<Account, Self::Error> {
Ok(self.config.base_account.base().to_owned())
}
fn date(&self, file: &Utf8Path) -> Option<Result<time::Date, Self::Error>> {
self.importer
.date(file, self)
.map(|result| result.map_err(Self::Error::from))
}
fn extract(
&self,
file: &Utf8Path,
existing: &[Directive],
) -> Result<Vec<Directive>, Self::Error> {
self.importer
.extract(file, existing, self)
.map_err(Self::Error::from)
}
fn filename(&self, _file: &Utf8Path) -> Option<Result<String, Self::Error>> {
Some(Ok(String::from("orders.csv")))
}
fn identify(&self, file: &Utf8Path) -> Result<bool, Self::Error> {
const EXPECTED_HEADERS: &[&str] = &[
"Website",
"Order ID",
"Order Date",
"Purchase Order Number",
"Currency",
"Unit Price",
"Unit Price Tax",
"Shipping Charge",
"Total Discounts",
"Total Owed",
"Shipment Item Subtotal",
"Shipment Item Subtotal Tax",
"ASIN",
"Product Condition",
"Quantity",
"Payment Instrument Type",
"Order Status",
"Shipment Status",
"Ship Date",
"Shipping Option",
"Shipping Address",
"Billing Address",
"Carrier Name & Tracking Number",
"Product Name",
"Gift Message",
"Gift Sender Name",
"Gift Recipient Contact Details",
];
self.importer
.identify(file, 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;
fn date(&self, record: &csv::Record) -> Option<Result<time::Date, Self::Error>> {
Some(parse_order_timestamp(record).map(OffsetDateTime::date))
}
fn extract(
&self,
_existing: &[Directive],
record: &csv::Record,
) -> Result<Vec<Directive>, Self::Error> {
const ASIN_COLUMN_INDEX: usize = 12;
const CURRENCY_COLUMN_INDEX: usize = 4;
const ORDER_ID_COLUMN_INDEX: usize = 1;
const PRODUCT_NAME_COLUMN_INDEX: usize = 23;
const TOTAL_OWED_COLUMN_INDEX: usize = 9;
let order_id = &record[ORDER_ID_COLUMN_INDEX];
let timestamp = parse_order_timestamp(record)?;
let commodity = Commodity::try_from(&record[CURRENCY_COLUMN_INDEX]).unwrap();
let amount = {
let amount = Decimal::from_str(&record[TOTAL_OWED_COLUMN_INDEX])
.unwrap()
.tap_mut(|amount| amount.rescale(2));
Amount::new(amount, commodity)
};
let asin = &record[ASIN_COLUMN_INDEX];
let product_name = &record[PRODUCT_NAME_COLUMN_INDEX];
let transaction = Transaction::on(timestamp.date()).tap_mut(|transaction| {
let context = {
let currency: &str = &commodity;
// TODO make this conversion in beancount_types
let currency = <&Seg>::try_from(currency).expect("commodities are valid segments");
TemplateContext { currency }
};
transaction
.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)
.add_meta(common_keys::TIMESTAMP, timestamp.format(&Rfc3339).unwrap())
.build_posting(self.config.base_account.render(&context), |posting| {
posting.set_amount(amount);
})
// TODO handle auto matching
.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)]
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,
}
fn parse_order_timestamp(record: &csv::Record) -> Result<OffsetDateTime, Error> {
const ORDER_DATE_COLUMN_INDEX: usize = 2;
parse_timestamp(&record[ORDER_DATE_COLUMN_INDEX])
}
fn parse_timestamp(input: &str) -> Result<OffsetDateTime, Error> {
const ORDER_DATE_FORMAT: &[time::format_description::FormatItem] =
format_description!("[month]/[day]/[year] [hour]:[minute]:[second] UTC");
let utc_timestamp = PrimitiveDateTime::parse(input, ORDER_DATE_FORMAT)
.unwrap()
.assume_utc();
let timezone = time_tz::system::get_timezone().expect("system timezone");
let timestamp = utc_timestamp.to_timezone(timezone);
Ok(timestamp)
}
pub mod digital_items;
pub mod gift_card_balance;
pub mod order_history;
use core::fmt::Display;
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 camino::Utf8Path;
use derive_builder::Builder;
use miette::Diagnostic;
use miette::IntoDiagnostic as _;
use rust_decimal::Decimal;
use serde::Deserialize;
use snafu::Backtrace;
use snafu::OptionExt;
use snafu::Snafu;
use tap::Tap as _;
use time::macros::format_description;
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 balance_account: AccountTemplate<TemplateSelector>,
pub claim_account: AccountTemplate<TemplateSelector>,
pub payment_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/gift-card-balance";
pub fn new(config: Config) -> Self {
let importer = csv::Importer::default();
Self { config, importer }
}
pub fn builder() -> ImporterBuilder {
ImporterBuilder::default()
}
}
impl Importer {
fn build_claim_transaction(
&self,
transaction: &mut Transaction,
claim_code: &str,
amount: Amount,
) {
let context = {
let currency: &str = &amount.commodity;
// TODO make this conversion in beancount_types
let currency = <&Seg>::try_from(currency).expect("commodities are valid segments");
TemplateContext { currency }
};
transaction
.add_meta(MetadataKey::try_from("claim-code").unwrap(), claim_code)
.build_posting(self.config.balance_account.render(&context), |posting| {
posting.set_amount(amount);
})
.build_posting(self.config.claim_account.render(&context), |_| {});
}
fn build_payment_transaction(
&self,
transaction: &mut Transaction,
order_id: &str,
amount: Amount,
) {
let context = {
let currency: &str = &amount.commodity;
// TODO make this conversion in beancount_types
let currency = <&Seg>::try_from(currency).expect("commodities are valid segments");
TemplateContext { currency }
};
transaction
.add_link(Link::try_from(format!("^amazon.{order_id}")).expect("valid link"))
.add_meta(MetadataKey::try_from("order-id").unwrap(), order_id)
.build_posting(self.config.balance_account.render(&context), |posting| {
posting.set_amount(amount);
})
.build_posting(self.config.payment_account.render(&context), |_posting| {});
}
}
impl beancount_importers_framework::ImporterProtocol for Importer {
type Error = miette::Report;
fn account(&self, _file: &Utf8Path) -> Result<Account, Self::Error> {
Ok(self.config.balance_account.base().to_owned())
}
fn date(&self, file: &Utf8Path) -> Option<Result<Date, Self::Error>> {
self.importer
.date(file, self)
.map(|result| result.map_err(Self::Error::from))
}
fn extract(
&self,
file: &Utf8Path,
_existing: &[Directive],
) -> Result<Vec<Directive>, Self::Error> {
self.importer
.extract(file, _existing, self)
.map_err(miette::Report::from)
}
fn filename(&self, _file: &Utf8Path) -> Option<Result<String, Self::Error>> {
Some(Ok(String::from("gift-card-transactions.csv")))
}
fn identify(&self, file: &Utf8Path) -> Result<bool, Self::Error> {
const EXPECTED_HEADERS: &[&str] = &["Date", "Description", "Amount"];
self.importer
.identify(file, 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;
fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
Some(parse_record_date(record))
}
fn extract(
&self,
_existing: &[Directive],
record: &csv::Record,
) -> Result<Vec<Directive>, Self::Error> {
let date = parse_record_date(record)?;
let kind = TransactionKind::try_from(&record[1]).unwrap();
let amount = {
let amount = &record[2];
let (negative, amount) = amount
.strip_prefix('-')
.map_or((false, amount), |amount| (true, amount));
let amount = amount.strip_prefix('€').unwrap();
let commodity = Commodity::try_from("EUR").unwrap();
let amount = Decimal::from_str(amount).unwrap().tap_mut(|decimal| {
decimal.rescale(2);
});
let amount = if negative { -amount } else { amount };
Amount::new(amount, commodity)
};
let transaction = Transaction::on(date).tap_mut(|transaction| match kind {
TransactionKind::Claim { claim_code } => {
self.build_claim_transaction(transaction, claim_code, amount);
}
TransactionKind::Payment { order_id } => {
self.build_payment_transaction(transaction, order_id, amount);
}
TransactionKind::TopUp { order_id } => {
self.build_payment_transaction(transaction, order_id, amount);
}
});
Ok(vec![Directive::from(transaction)])
}
}
impl ImporterBuilder {
pub fn build(&self) -> Result<Importer, ImporterBuilderError> {
let config = Config {
balance_account: self
.balance_account
.clone()
.context(UninitializedFieldSnafu {
field: "balance_account",
importer: Importer::NAME,
})?,
claim_account: self
.claim_account
.clone()
.context(UninitializedFieldSnafu {
field: "claim_account",
importer: Importer::NAME,
})?,
payment_account: self
.payment_account
.clone()
.context(UninitializedFieldSnafu {
field: "payment_account",
importer: Importer::NAME,
})?,
};
Ok(Importer::new(config))
}
}
fn parse_record_date(record: &csv::Record) -> Result<Date, Error> {
const DATE_COLUMN_INDEX: usize = 0;
const DATE_FORMAT: &[time::format_description::FormatItem] =
format_description!("[day] [month repr:long] [year]");
Date::parse(&record[DATE_COLUMN_INDEX], DATE_FORMAT).map_err(|_| todo!())
}
#[derive(Clone, Copy, Debug)]
pub struct TemplateContext<'c> {
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 {
selector: String,
backtrace: Backtrace,
}
#[derive(Clone, Copy, Debug)]
enum TransactionKind<'d> {
Claim { claim_code: &'d str },
Payment { order_id: &'d str },
TopUp { order_id: &'d str },
}
impl Display for TransactionKind<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransactionKind::Claim { .. } => f.write_str("Gift Card Claim"),
TransactionKind::Payment { .. } => f.write_str("Payment"),
TransactionKind::TopUp { .. } => f.write_str("Balance Top Up"),
}
}
}
impl<'d> TryFrom<&'d str> for TransactionKind<'d> {
type Error = (); // TODO
fn try_from(description: &'d str) -> Result<Self, Self::Error> {
if let Some(rest) = description.strip_prefix("Gift Card claim") {
let (_, rest) = rest.rsplit_once('(').unwrap();
let rest = rest.strip_suffix(')').unwrap();
let claim_code = rest.strip_prefix("claim code ").unwrap();
Ok(Self::Claim { claim_code })
} else if let Some(rest) = description.strip_prefix("Balance Top Up") {
let (_, rest) = rest.rsplit_once('(').unwrap();
let order_id = rest.strip_suffix(')').unwrap();
Ok(Self::TopUp { order_id })
} else if let Some(rest) = description.strip_prefix("Payment") {
let (_, rest) = rest.rsplit_once('(').unwrap();
let order_id = rest.strip_suffix(')').unwrap();
Ok(Self::Payment { order_id })
} else {
todo!("unsupported description {description:?}")
}
}
}
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 camino::Utf8Path;
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::macros::format_description;
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, _file: &Utf8Path) -> Result<Account, Self::Error> {
Ok(self.config.base_account.base().to_owned())
}
fn date(&self, file: &Utf8Path) -> Option<Result<time::Date, Self::Error>> {
self.importer
.date(file, self)
.map(|result| result.map_err(Self::Error::from))
}
fn extract(
&self,
file: &Utf8Path,
existing: &[Directive],
) -> Result<Vec<Directive>, Self::Error> {
self.importer
.extract(file, existing, self)
.map_err(Self::Error::from)
}
fn filename(&self, _file: &Utf8Path) -> Option<Result<String, Self::Error>> {
Some(Ok(String::from("digital-items.csv")))
}
fn identify(&self, file: &Utf8Path) -> 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(file, 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;
fn date(&self, record: &csv::Record) -> Option<Result<time::Date, Self::Error>> {
Some(parse_order_date(record))
}
fn extract(
&self,
_existing: &[Directive],
record: &csv::Record,
) -> Result<Vec<Directive>, Self::Error> {
const ASIN_COLUMN_INDEX: usize = 0;
const CURRENCY_COLUMN_INDEX: usize = 14;
const ORDER_ID_COLUMN_INDEX: usize = 2;
const PRODUCT_NAME_COLUMN_INDEX: usize = 1;
const TOTAL_OWED_COLUMN_INDEX: usize = 13;
let date = parse_order_date(record)?;
let asin = &record[ASIN_COLUMN_INDEX];
let product_name = &record[PRODUCT_NAME_COLUMN_INDEX];
let order_id = &record[ORDER_ID_COLUMN_INDEX];
let commodity = Commodity::try_from(&record[CURRENCY_COLUMN_INDEX]).unwrap();
let amount = {
let amount = Decimal::from_str(&record[TOTAL_OWED_COLUMN_INDEX])
.unwrap()
.tap_mut(|amount| amount.rescale(2));
Amount::new(amount, commodity)
};
let transaction = Transaction::on(date).tap_mut(|transaction| {
let context = {
let currency: &str = &commodity;
// TODO make this conversion in beancount_types
let currency = <&Seg>::try_from(currency).expect("commodities are valid segments");
TemplateContext { currency }
};
transaction
.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);
})
// TODO handle auto matching
.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)]
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,
}
fn parse_order_date(record: &csv::Record) -> Result<Date, Error> {
const ORDER_DATE_COLUMN_INDEX: usize = 9;
parse_date(&record[ORDER_DATE_COLUMN_INDEX])
}
fn parse_date(input: &str) -> Result<Date, Error> {
use time::format_description::FormatItem;
const ORDER_DATE_FORMAT: &[FormatItem] = format_description!("[year]-[month]-[day]");
Date::parse(input, ORDER_DATE_FORMAT).map_err(|_| todo!())
}
[package]
name = "amazon"
authors.workspace = true
edition.workspace = true
publish.workspace = true
rust-version.workspace = true
version.workspace = true
[dependencies]
# Workspace dependencies
beancount-importers-framework.path = "../../framework"
beancount-types.path = "../../common/beancount-types"
csv.path = "../csv"
german-decimal.path = "../../common/german-decimal"
# Inherited dependencies
camino.workspace = true
derive_builder.workspace = true
miette.workspace = true
rust_decimal.workspace = true
serde.workspace = true
snafu.workspace = true
tap.workspace = true
time-tz.workspace = true
time.workspace = true
use miette::Diagnostic;
use snafu::Backtrace;
use snafu::Snafu;
#[derive(Debug, Diagnostic, Snafu)]
#[snafu(visibility(pub))]
pub enum ImporterBuilderError {
#[snafu(display("importer `{importer}`: `{field}` must be initialized"))]
UninitializedField {
backtrace: Backtrace,
field: &'static str,
importer: &'static str,
},
}