P3: dated 1099 decisions, payment methods and the card exception (defects 23, 12, 18, D-40)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
A vendor's 1099 decision is its own dated fact (files NEC or MISC, not reportable with its reason, or undetermined), applied by each payment's date. Payment methods are recorded; card and network payments are left out and listed; a member's check through financing counts; a bank payment with no method counts (D-40); $2,000 for payments after 2025. One worksheet for the tool, the viewer and the CPA package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
77 files changed
+11455
-970
No files matched your search
@@ -62,7 +62,7 @@ const C: Needs = PostWhenConfirmed;
|
||||
const K: Needs = PostForApprovalRules;
|
||||
|
||||
/// Every tool and what it needs (INVARIANT 1).
|
||||
pub const TABLE: [(&str, Needs); 112] = [
|
||||
pub const TABLE: [(&str, Needs); 115] = [
|
||||
("books.handbook", R),
|
||||
("books.start", R),
|
||||
// Settings are committed at once: the settings commit of the plan.
|
||||
@@ -116,6 +116,9 @@ pub const TABLE: [(&str, Needs); 112] = [
|
||||
("books.list_vendors", R),
|
||||
("books.set_vendor_1099", W),
|
||||
("books.set_entry_vendor", W),
|
||||
("books.add_instrument", W),
|
||||
("books.instruments", R),
|
||||
("books.set_entry_instrument", W),
|
||||
// Until P7 these can put full TINs out (with include_tins, or a W-9
|
||||
// itself): only an admin token, never a plain one (plan 3.3, "no
|
||||
// scope unwraps the vault", holds for every other scope).
|
||||
|
||||
+177
-62
@@ -557,7 +557,7 @@ fn vendor_1099_report(
|
||||
year: i32,
|
||||
threshold: Option<Decimal>,
|
||||
) -> Result<ledger_core::Vendor1099Report, ledger_core::LedgerError> {
|
||||
ledger_package::vendor_1099_report(ledger, &package_settings(settings), year, None, threshold)
|
||||
ledger_package::vendor_1099_report(ledger, &package_settings(settings), year, threshold, None)
|
||||
}
|
||||
|
||||
impl BooksServer {
|
||||
@@ -750,6 +750,7 @@ impl BooksServer {
|
||||
+ Self::access_router()
|
||||
+ Self::files_router()
|
||||
+ Self::backup_router()
|
||||
+ Self::instruments_router()
|
||||
}
|
||||
|
||||
/// The settings in force for this request (reloaded at its start, in
|
||||
@@ -915,8 +916,10 @@ impl BooksServer {
|
||||
/// Cash and card accounts due for reconciliation
|
||||
/// (ledger_store::reconcilable, the viewer's Needs-attention rule):
|
||||
/// the cash accounts (`[books] cash_accounts`, else assets named for
|
||||
/// cash), the card accounts (liabilities named "card", before their
|
||||
/// first statement), and every other liability with a recorded
|
||||
/// cash), the card accounts (ledger_core::parties::card_accounts: the
|
||||
/// business-card payment methods' liabilities, else liabilities named
|
||||
/// "card", not "gift", before their first statement), and every other
|
||||
/// liability with a recorded
|
||||
/// statement, with posted activity through the end of last month, not
|
||||
/// reconciled through it.
|
||||
///
|
||||
@@ -2729,6 +2732,8 @@ mod access_tools;
|
||||
mod backup_tools;
|
||||
#[path = "tools/files.rs"]
|
||||
mod files;
|
||||
#[path = "tools/instruments.rs"]
|
||||
mod instruments;
|
||||
#[path = "tools/kept.rs"]
|
||||
mod kept;
|
||||
pub use kept::SettingKeyOut;
|
||||
@@ -3025,6 +3030,11 @@ impl BooksServer {
|
||||
.as_deref()
|
||||
.map(|v| resolve_vendor(&store, v))
|
||||
.transpose()?;
|
||||
let instrument = p
|
||||
.instrument
|
||||
.as_deref()
|
||||
.map(|r| instruments::resolve_instrument(&store, r))
|
||||
.transpose()?;
|
||||
let party = p
|
||||
.party
|
||||
.as_deref()
|
||||
@@ -3064,6 +3074,10 @@ impl BooksServer {
|
||||
));
|
||||
}
|
||||
}
|
||||
// A payment method exists only under the payment_instruments
|
||||
// feature (books.add_instrument declared it), so tagging the draft
|
||||
// with one below declares nothing after the draft's first line
|
||||
// (B-27).
|
||||
let id = store
|
||||
.create_entry(date, &p.description)
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -3082,6 +3096,11 @@ impl BooksServer {
|
||||
} else if let Some(r) = no_party {
|
||||
store.set_entry_no_party(id, r).map_err(|e| e.to_string())?;
|
||||
}
|
||||
if instrument.is_some() {
|
||||
store
|
||||
.set_entry_instrument(id, instrument)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(Json(DraftEntryOut {
|
||||
entry_id: id,
|
||||
status: "draft".to_string(),
|
||||
@@ -3901,6 +3920,12 @@ impl BooksServer {
|
||||
.entry_vendor(p.entry_id)
|
||||
.and_then(|v| store.ledger().get_vendor(v))
|
||||
.map(|v| v.name.clone()),
|
||||
instrument: store
|
||||
.ledger()
|
||||
.entry_instrument(p.entry_id)
|
||||
.and_then(|i| store.ledger().get_instrument(i))
|
||||
.map(|i| i.label.clone()),
|
||||
instrument_id: store.ledger().entry_instrument(p.entry_id),
|
||||
party_id: store.ledger().entry_party(p.entry_id),
|
||||
party: store
|
||||
.ledger()
|
||||
@@ -4869,7 +4894,7 @@ impl BooksServer {
|
||||
#[tool(
|
||||
name = "books.list_vendors",
|
||||
title = "List vendors",
|
||||
description = "Vendors, with which 1099 each gets, whether its W-9 is on file, and the party each belongs to (books.party).",
|
||||
description = "Vendors, each with its 1099 decision in force today (nec, misc, not_reportable with its reason, or undetermined; who decided and when) and every dated decision, whether its W-9 is on file, and the party it belongs to (books.party). undetermined with no decided_by: nobody has decided yet (books.set_vendor_1099).",
|
||||
annotations(read_only_hint = true, open_world_hint = false)
|
||||
)]
|
||||
async fn list_vendors(&self) -> Result<Json<ListVendorsOut>, String> {
|
||||
@@ -4879,51 +4904,59 @@ impl BooksServer {
|
||||
.ledger()
|
||||
.list_vendors()
|
||||
.into_iter()
|
||||
.map(|v| VendorOut {
|
||||
id: v.id,
|
||||
name: v.name.clone(),
|
||||
form_1099: form_1099_label(v.form_1099),
|
||||
tin_on_file: v.tin_on_file,
|
||||
party_id: store.ledger().party_of_vendor(v.id),
|
||||
})
|
||||
.map(|v| instruments::vendor_out(&store, v))
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "books.set_vendor_1099",
|
||||
title = "Set a vendor's 1099 status",
|
||||
description = "Mark which information return a vendor gets: \"nec\" (1099-NEC: contractors, freelancers, attorneys), \"misc\" (1099-MISC: rent, royalties, prizes), or \"none\" (corporations generally, merchants, anyone paid only by card). Record whether a W-9 is on file; never store the TIN itself. Ask the human when unsure; the CPA confirms.",
|
||||
title = "Record a vendor's 1099 decision",
|
||||
description = "Record whether a vendor gets a 1099, with who decided (decided_by): nec (1099-NEC: contractors, attorneys), misc (1099-MISC: rent, royalties), not_reportable with reason_kind (corporation, tax_exempt, united_states, state, foreign_government, foreign_person, merchandise, employee, other with a citation) and reason in words, or undetermined. A corporation paid for legal or medical services stays nec/misc. Paid by card is never a reason (cards are excluded per payment). effective: only when the payee's status changed (an LLC's corporate election, from its date); a payee that incorporates is a new vendor. tin_on_file: a W-9 is on file (alone, it keeps the decision). Ask the human; CPA confirms.",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = false,
|
||||
idempotent_hint = true,
|
||||
open_world_hint = false
|
||||
)
|
||||
)]
|
||||
async fn set_vendor_1099(
|
||||
&self,
|
||||
params: Parameters<SetVendor1099Params>,
|
||||
) -> Result<Json<VendorOut>, String> {
|
||||
) -> Result<Json<SetVendor1099Out>, String> {
|
||||
let p = params.0;
|
||||
let mut store = self.lock()?;
|
||||
let id = resolve_vendor(&store, &p.vendor)?;
|
||||
let form = match p.form_1099.trim().to_lowercase().as_str() {
|
||||
"none" | "" => None,
|
||||
other => Some(
|
||||
ledger_core::Form1099::parse(other)
|
||||
.ok_or_else(|| format!("form_1099 {other:?}: expected nec, misc, or none"))?,
|
||||
),
|
||||
let decision = instruments::decision_in(&p)?;
|
||||
let (changed, warnings) = match decision {
|
||||
None => {
|
||||
let flag = p.tin_on_file.expect("decision_in: one of the two");
|
||||
store
|
||||
.set_vendor_w9_flag(id, flag)
|
||||
.map_err(|e| e.to_string())?;
|
||||
(ledger_core::form1099::Applied::Unchanged, Vec::new())
|
||||
}
|
||||
Some(d) => {
|
||||
let reached = ledger_core::form1099::decision_changes(store.ledger(), id, &d)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let applied = store
|
||||
.set_vendor_1099(id, &d, p.tin_on_file)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let name = &store.ledger().get_vendor(id).expect("resolved").name;
|
||||
let warnings = instruments::filed_year_warnings(
|
||||
store.ledger(),
|
||||
&format!("{name}'s 1099 decision"),
|
||||
&reached,
|
||||
today(),
|
||||
);
|
||||
(applied, warnings)
|
||||
}
|
||||
};
|
||||
store
|
||||
.set_vendor_1099(id, form, p.tin_on_file)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let v = store.ledger().get_vendor(id).expect("exists");
|
||||
Ok(Json(VendorOut {
|
||||
id,
|
||||
name: v.name.clone(),
|
||||
form_1099: form_1099_label(v.form_1099),
|
||||
tin_on_file: v.tin_on_file,
|
||||
party_id: store.ledger().party_of_vendor(id),
|
||||
Ok(Json(SetVendor1099Out {
|
||||
vendor: instruments::vendor_out(&store, v),
|
||||
changed: instruments::applied_key(changed).to_string(),
|
||||
warnings,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -4962,7 +4995,7 @@ impl BooksServer {
|
||||
#[tool(
|
||||
name = "books.vendor_1099",
|
||||
title = "1099 worksheet",
|
||||
description = "1099 worksheet for a calendar year: cash paid to each vendor (credits minus debits to the [books] cash and financing accounts on entries tagged with the vendor, so card payments, which the card processor reports on 1099-K, are excluded; on an entry naming several parties, only the vendor's resolved share of that payment), which vendors are reportable (marked NEC/MISC and paid at least the threshold: $600 through 2025, $2,000 in 2026, indexed for inflation after that), which are missing a W-9, and each payee's TIN last four. include_tins: true adds each payee's full TIN from the TIN vault (or why it can't), for the CPA's worksheet; csv: true also hands it over as a CSV file (run its download command to save it). For a year whose threshold wanderbooks doesn't know yet it refuses rather than guess: get the figure from the IRS or the human, pass it as `threshold`, and tell the human, who can report it upstream so the rule is updated for everyone. For the CPA to confirm.",
|
||||
description = "The 1099 worksheet for a calendar year. Per vendor: what counts toward its 1099 (paid out of the cash accounts, or through a financing account with the method recorded, while not decided not_reportable; an entry with several parties counts only the vendor's share), what was paid by card or payment network (excluded: Form 1099-K's), what is undetermined (a member paid, method not recorded: books.set_entry_instrument), its 1099 decision, and its status (reportable at the threshold: $600 through 2025, $2,000 in 2026, indexed after; under_threshold; not_reportable; undetermined). Lists those payments, W-9s missing, each payee's TIN last four. include_tins adds full TINs from the vault; csv hands it over. cash_accounts and financing_accounts default to the settings' (as books.export_cpa_package's do); through stops the year at a day: given a package's accounts and its as_of, the CSV is the package's 1099 worksheet. A year whose threshold isn't known is refused: pass threshold from the IRS. For the CPA to confirm.",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = false,
|
||||
@@ -4975,21 +5008,44 @@ impl BooksServer {
|
||||
) -> Result<Json<Vendor1099Out>, String> {
|
||||
let p = params.0;
|
||||
let threshold = p.threshold.as_deref().map(parse_amount).transpose()?;
|
||||
let through = p.through.as_deref().map(parse_date).transpose()?;
|
||||
if let Some(t) = through
|
||||
&& chrono::Datelike::year(&t) != p.year
|
||||
{
|
||||
return Err(format!(
|
||||
"through ({t}) is a day of {}: the worksheet counts payments made in {} from January 1 through it",
|
||||
p.year, p.year
|
||||
));
|
||||
}
|
||||
let store = self.lock()?;
|
||||
let ledger = store.ledger();
|
||||
for code in p.cash_accounts.iter().chain(&p.financing_accounts) {
|
||||
if ledger.chart().find_by_code(code).is_none() {
|
||||
return Err(format!(
|
||||
"no account {code} in the chart (books.chart lists them)"
|
||||
));
|
||||
}
|
||||
}
|
||||
// With include_tins, the vault is opened once (never creating a
|
||||
// key: a locked vault becomes a note on each line).
|
||||
let vault = p.include_tins.then(|| self.open_vault(&store, false));
|
||||
let tins = vault
|
||||
.as_ref()
|
||||
.map(|vault| move |id| vault_tin(ledger, vault, id));
|
||||
// The accounts given, else the settings': the CPA package's rule
|
||||
// (ledger_package::Settings::with_accounts), so its 1099 worksheet
|
||||
// is this one for the same accounts and `through` its as_of.
|
||||
let settings = self.settings();
|
||||
let accounts =
|
||||
package_settings(&settings).with_accounts(&p.cash_accounts, &p.financing_accounts);
|
||||
// A year past what wanderbooks knows is refused with guidance
|
||||
// (research or ask, pass `threshold`, then update the rule).
|
||||
let w = ledger_package::vendor_1099_worksheet(
|
||||
ledger,
|
||||
&package_settings(&self.settings()),
|
||||
&accounts,
|
||||
p.year,
|
||||
threshold,
|
||||
through,
|
||||
tins.as_ref().map(|f| f as ledger_package::TinSource<'_>),
|
||||
)?;
|
||||
let warning = p.include_tins.then(|| {
|
||||
@@ -5006,6 +5062,27 @@ impl BooksServer {
|
||||
Some(self.hand_over(&book_name(&store), &name, "text/csv", csv.into_bytes())?)
|
||||
}
|
||||
};
|
||||
let payments = |list: &[ledger_core::Payment1099]| -> Vec<Payment1099Out> {
|
||||
list.iter()
|
||||
.map(|p| Payment1099Out {
|
||||
entry_id: p.entry_id,
|
||||
date: p.date.to_string(),
|
||||
vendor_id: p.vendor_id,
|
||||
vendor: ledger
|
||||
.get_vendor(p.vendor_id)
|
||||
.map(|v| v.name.clone())
|
||||
.unwrap_or_default(),
|
||||
amount: money(p.amount),
|
||||
instrument: p
|
||||
.instrument
|
||||
.and_then(|i| ledger.get_instrument(i))
|
||||
.map(|i| i.label.clone()),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let excluded_card_payments = payments(&w.excluded_card_payments);
|
||||
let undetermined_payments = payments(&w.undetermined_payments);
|
||||
let other_receipts = payments(&w.other_receipts);
|
||||
let vendors = w
|
||||
.lines
|
||||
.into_iter()
|
||||
@@ -5013,8 +5090,23 @@ impl BooksServer {
|
||||
vendor_id: l.vendor_id,
|
||||
name: l.name,
|
||||
form_1099: l.form_1099,
|
||||
decision_1099: instruments::decision_out(Some(&ledger_core::Dated1099 {
|
||||
effective: l.effective,
|
||||
decision: l.decision.clone(),
|
||||
source: l.decided.clone(),
|
||||
})),
|
||||
decisions_in_year: l
|
||||
.decisions
|
||||
.iter()
|
||||
.map(|d| instruments::decision_out(Some(d)))
|
||||
.collect(),
|
||||
tin_on_file: l.tin_on_file,
|
||||
paid: money(l.paid),
|
||||
excluded_card: money(l.excluded_card),
|
||||
undetermined: money(l.undetermined),
|
||||
not_reportable: money(l.not_reportable),
|
||||
status: l.status.key().to_string(),
|
||||
decision_needed: l.decision_needed,
|
||||
reportable: l.reportable,
|
||||
party_id: l.party_id,
|
||||
tin_last4: l.tin_last4,
|
||||
@@ -5028,6 +5120,11 @@ impl BooksServer {
|
||||
threshold: money(w.threshold),
|
||||
vendors,
|
||||
missing_w9: w.missing_w9,
|
||||
excluded_card_payments,
|
||||
undetermined_payments,
|
||||
other_receipts,
|
||||
untagged_entries: w.untagged_payments,
|
||||
unresolved: w.unresolved,
|
||||
notes: w.notes,
|
||||
warning,
|
||||
csv: csv_file,
|
||||
@@ -7005,6 +7102,15 @@ impl BooksServer {
|
||||
.set_party_creditor(id, *a, *on)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
// The creditor lines' journal feature, declared before this
|
||||
// command's first line, never between its lines (rules.md B-27: a
|
||||
// refused format upgrade must not leave the name changed and the
|
||||
// creditors not).
|
||||
if !creditor.is_empty() {
|
||||
store
|
||||
.ensure_feature(ledger_store::format::PARTY_CREDITORS)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
for f in fields {
|
||||
store.update_party(id, f).map_err(|e| e.to_string())?;
|
||||
}
|
||||
@@ -7299,6 +7405,19 @@ impl BooksServer {
|
||||
}
|
||||
};
|
||||
let on_file = facts.is_some();
|
||||
// The vendor record's W-9 flag follows (below): its feature is
|
||||
// declared before this command's first line, never after it
|
||||
// (B-27: a refusal to upgrade the format must not leave the W-9
|
||||
// recorded and the flag not).
|
||||
let vendor = store.ledger().get_party(id).and_then(|p| p.vendor);
|
||||
if vendor
|
||||
.and_then(|v| store.ledger().get_vendor(v))
|
||||
.is_some_and(|v| v.tin_on_file != on_file)
|
||||
{
|
||||
store
|
||||
.ensure_feature(ledger_store::format::VENDOR_1099_DECISIONS)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
if replace_tin {
|
||||
store.set_party_tin(id, None).map_err(|e| e.to_string())?;
|
||||
}
|
||||
@@ -7308,18 +7427,11 @@ impl BooksServer {
|
||||
.set_party_tin(id, tin_facts)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
// Keep the vendor record's W-9 flag (the 1099 worksheet's) in step.
|
||||
let vendor = store
|
||||
.ledger()
|
||||
.get_party(id)
|
||||
.and_then(|p| p.vendor)
|
||||
.and_then(|v| store.ledger().get_vendor(v))
|
||||
.map(|v| (v.id, v.form_1099, v.tin_on_file));
|
||||
if let Some((v, form, tin_on_file)) = vendor
|
||||
&& tin_on_file != on_file
|
||||
{
|
||||
// Keep the vendor record's W-9 flag (the 1099 worksheet's) in
|
||||
// step, its 1099 decision and who made it kept (P3 review R8).
|
||||
if let Some(v) = vendor {
|
||||
store
|
||||
.set_vendor_1099(v, form, on_file)
|
||||
.set_vendor_w9_flag(v, on_file)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
let party = store.ledger().get_party(id).expect("resolved");
|
||||
@@ -7722,20 +7834,22 @@ impl BooksServer {
|
||||
entries.sort_by(|a, b| b.date.cmp(&a.date).then(b.entry_id.cmp(&a.entry_id)));
|
||||
let invoices = party_invoices(ledger, party);
|
||||
let bills = party_bills(ledger, party);
|
||||
let amount_1099 = match party.vendor {
|
||||
Some(v) => {
|
||||
let r = vendor_1099_report(ledger, &self.settings(), year, Some(Decimal::ZERO))
|
||||
.map_err(|e| e.to_string())?;
|
||||
Some(money(
|
||||
r.vendors
|
||||
.iter()
|
||||
.find(|l| l.vendor_id == v)
|
||||
.map(|l| l.paid)
|
||||
.unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
// The worksheet's figure; none when it can't be made (no cash
|
||||
// accounts named), never a failed answer.
|
||||
let amount_1099 = party.vendor.and_then(|v| {
|
||||
let r = vendor_1099_report(ledger, &self.settings(), year, Some(Decimal::ZERO)).ok()?;
|
||||
Some(money(
|
||||
r.vendors
|
||||
.iter()
|
||||
.find(|l| l.vendor_id == v)
|
||||
.map(|l| l.paid)
|
||||
.unwrap_or_default(),
|
||||
))
|
||||
});
|
||||
let decided_now = party
|
||||
.vendor
|
||||
.and_then(|v| ledger.get_vendor(v))
|
||||
.map(|v| v.decision_on(today()).cloned().unwrap_or_default());
|
||||
// Documents and runs are restored as journaled (rules.md B-28): a
|
||||
// total past exact arithmetic refuses the answer, never a panic.
|
||||
let mut payroll = Vec::new();
|
||||
@@ -7780,10 +7894,8 @@ impl BooksServer {
|
||||
"what is receivable",
|
||||
)?),
|
||||
amount_1099,
|
||||
form_1099: party
|
||||
.vendor
|
||||
.and_then(|v| ledger.get_vendor(v))
|
||||
.and_then(|v| form_1099_label(v.form_1099)),
|
||||
form_1099: form_1099_label(decided_now.as_ref().and_then(|d| d.form())),
|
||||
decision_1099: decided_now.as_ref().map(|d| d.key().to_string()),
|
||||
gross_pay: money(gross),
|
||||
net_pay: money(net),
|
||||
};
|
||||
@@ -10063,7 +10175,7 @@ mod tests {
|
||||
const D: Hints = (Some(false), Some(true), None);
|
||||
const WI: Hints = (Some(false), Some(false), Some(true));
|
||||
const DI: Hints = (Some(false), Some(true), Some(true));
|
||||
let table: [(&str, Hints); 112] = [
|
||||
let table: [(&str, Hints); 115] = [
|
||||
("books.handbook", R),
|
||||
("books.start", R),
|
||||
("books.set_setting", DI),
|
||||
@@ -10113,9 +10225,12 @@ mod tests {
|
||||
("books.run_recurring", DI),
|
||||
("books.record_payout", W),
|
||||
("books.list_vendors", R),
|
||||
("books.set_vendor_1099", W),
|
||||
("books.set_vendor_1099", WI),
|
||||
("books.set_entry_vendor", W),
|
||||
("books.vendor_1099", W),
|
||||
("books.add_instrument", W),
|
||||
("books.instruments", R),
|
||||
("books.set_entry_instrument", WI),
|
||||
("books.chart_template", R),
|
||||
("books.tax_categories", R),
|
||||
("books.renumber_account", D),
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
//! 1099 tracking through the server: payment methods
|
||||
//! (`books.add_instrument`, `books.instruments`,
|
||||
//! `books.set_entry_instrument`), and the 1099 decision's shape in and out
|
||||
//! (`books.set_vendor_1099`, `books.list_vendors`, `books.vendor_1099`;
|
||||
//! plan P3, defects 12, 18 and 23; D-40).
|
||||
//!
|
||||
//! ```text
|
||||
//! GOAL: the agent records how each payment was made and why each vendor
|
||||
//! does or doesn't get a 1099, and the worksheet never guesses.
|
||||
//! SOURCE: docs/spec/rules.md B-19 (Treas. Reg. §1.6041-1(a)(1)(iv),
|
||||
//! §1.6041-3; sources/irs-1099-payment-method.md); plan/served.md
|
||||
//! 4.9; DECISION D-40 (owner, 2026-10-20).
|
||||
//! INVARIANTS:
|
||||
//! 1. A 1099 decision names who made it (decided_by; decided_on
|
||||
//! defaults to today in the company's time zone). "none" is refused:
|
||||
//! no 1099 is not_reportable with its reason, or undetermined.
|
||||
//! 2. not_reportable takes a reason_kind and a reason, and other takes
|
||||
//! a citation too; no other decision takes either.
|
||||
//! 3. tin_on_file omitted keeps the W-9 flag; form_1099 omitted changes
|
||||
//! only it, keeping the decision and who made it (P3 review R8).
|
||||
//! 4. A change that reaches payments in a closed period, or in a year
|
||||
//! whose 1099s were due (January 31 of the next year), warns.
|
||||
//! 5. A payment method is named by id or label; a number that is one
|
||||
//! method's id and another's label is refused.
|
||||
//! ```
|
||||
|
||||
use super::*;
|
||||
use ledger_core::form1099::{Applied, Dated1099, Decision1099, ExemptReason, Form1099};
|
||||
use ledger_core::{InstrumentId, InstrumentKind};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Parameters and results
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/// Parameters for `books.add_instrument`.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct AddInstrumentParams {
|
||||
/// bank (the company's checks, ACH, wires, bill-pay), business_card (a
|
||||
/// company card the payee charged), member_card (a member's personal
|
||||
/// card the payee charged), member_cash_or_check (a member's own cash,
|
||||
/// check, ACH, Zelle, or a friends-and-family transfer), or platform (a
|
||||
/// payment the payee received as a seller through PayPal or Venmo goods
|
||||
/// and services).
|
||||
pub kind: String,
|
||||
/// Its name ("Harbor Bank checking", "Riley's Visa"), unique.
|
||||
pub label: String,
|
||||
/// The last four digits of its card or account number (never more).
|
||||
pub last4: Option<String>,
|
||||
/// Whose it is (a member's card): party id or name. Informational.
|
||||
pub owner_party: Option<String>,
|
||||
/// The account it pays through (id or code): the bank account, the
|
||||
/// card's liability, or the members' financing account (Due to
|
||||
/// Members) for a member's own card or cash.
|
||||
pub account: String,
|
||||
}
|
||||
|
||||
/// A payment method.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct InstrumentOut {
|
||||
/// Its id.
|
||||
pub id: u64,
|
||||
/// Its kind (as books.add_instrument takes it).
|
||||
pub kind: String,
|
||||
/// Whether a payment made with it is a card or payment-network
|
||||
/// payment: excluded from the company's 1099s (Form 1099-K's).
|
||||
pub card_or_network: bool,
|
||||
/// Its name.
|
||||
pub label: String,
|
||||
/// Its last four digits, if recorded.
|
||||
pub last4: Option<String>,
|
||||
/// Whose it is: party id, if recorded.
|
||||
pub owner_party: Option<u64>,
|
||||
/// The account it pays through: id.
|
||||
pub account_id: u64,
|
||||
/// That account's code.
|
||||
pub account: String,
|
||||
}
|
||||
|
||||
/// Result of `books.add_instrument`.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct AddInstrumentOut {
|
||||
/// The payment method.
|
||||
#[serde(flatten)]
|
||||
pub instrument: InstrumentOut,
|
||||
/// Things to check, if any.
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Parameters for `books.instruments`.
|
||||
#[derive(Debug, Default, Deserialize, JsonSchema)]
|
||||
pub struct InstrumentsParams {}
|
||||
|
||||
/// Result of `books.instruments`.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct InstrumentsOut {
|
||||
/// Every payment method, by id.
|
||||
pub instruments: Vec<InstrumentOut>,
|
||||
}
|
||||
|
||||
/// Parameters for `books.set_entry_instrument`.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct SetEntryInstrumentParams {
|
||||
/// Entry id (draft or posted).
|
||||
pub entry_id: u64,
|
||||
/// The payment method (id or label, books.instruments); omit to clear.
|
||||
pub instrument: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of `books.set_entry_instrument`.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct SetEntryInstrumentOut {
|
||||
/// The entry, as books.get_entry shows it.
|
||||
#[serde(flatten)]
|
||||
pub entry: GetEntryOut,
|
||||
/// Whether anything changed (false: the entry and its reversals were
|
||||
/// tagged so already).
|
||||
pub changed: bool,
|
||||
/// Payments the change reaches in a closed period, or in a year whose
|
||||
/// 1099s were due: tell the human.
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Shapes
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/// A dated decision as the server shows it (none: never decided).
|
||||
pub(super) fn decision_out(d: Option<&Dated1099>) -> Decision1099Out {
|
||||
let undecided = Decision1099::Undetermined;
|
||||
let decision = d.map_or(&undecided, |d| &d.decision);
|
||||
let (reason_kind, reason, authority) = match decision {
|
||||
Decision1099::NotReportable { kind, reason } => (
|
||||
Some(kind.key().to_string()),
|
||||
Some(reason.clone()),
|
||||
kind.citation().map(str::to_string),
|
||||
),
|
||||
_ => (None, None, None),
|
||||
};
|
||||
let source = d.and_then(|d| d.source.as_ref());
|
||||
Decision1099Out {
|
||||
value: decision.key().to_string(),
|
||||
reason_kind,
|
||||
reason,
|
||||
authority,
|
||||
effective: d.and_then(|d| d.effective).map(|e| e.to_string()),
|
||||
decided_by: source.map(|s| s.decided_by.clone()),
|
||||
decided_on: source.map(|s| s.decided_on.to_string()),
|
||||
citation: source.and_then(|s| s.citation.clone()),
|
||||
note: source.and_then(|s| s.note.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A vendor as the server shows it: its decision in force today, and every
|
||||
/// one recorded.
|
||||
pub(super) fn vendor_out(store: &Store, v: &ledger_core::Vendor) -> VendorOut {
|
||||
let now = ledger_core::form1099::in_force(&v.decisions_1099, today());
|
||||
VendorOut {
|
||||
id: v.id,
|
||||
name: v.name.clone(),
|
||||
form_1099: form_1099_label(now.and_then(|d| d.decision.form())),
|
||||
decision_1099: decision_out(now),
|
||||
decisions_1099: v
|
||||
.decisions_1099
|
||||
.iter()
|
||||
.map(|d| decision_out(Some(d)))
|
||||
.collect(),
|
||||
tin_on_file: v.tin_on_file,
|
||||
party_id: store.ledger().party_of_vendor(v.id),
|
||||
}
|
||||
}
|
||||
|
||||
fn instrument_out(store: &Store, i: &ledger_core::Instrument) -> InstrumentOut {
|
||||
InstrumentOut {
|
||||
id: i.id,
|
||||
kind: i.kind.key().to_string(),
|
||||
card_or_network: i.kind.is_card_or_network(),
|
||||
label: i.label.clone(),
|
||||
last4: i.last4.clone(),
|
||||
owner_party: i.owner_party,
|
||||
account_id: i.account,
|
||||
account: store
|
||||
.ledger()
|
||||
.chart()
|
||||
.get(i.account)
|
||||
.map(|a| a.code.clone())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a payment method by id or label (INVARIANT 5).
|
||||
pub(super) fn resolve_instrument(store: &Store, r: &str) -> Result<InstrumentId, String> {
|
||||
let ledger = store.ledger();
|
||||
let by_id = r
|
||||
.trim()
|
||||
.parse::<u64>()
|
||||
.ok()
|
||||
.filter(|id| ledger.get_instrument(*id).is_some());
|
||||
let by_label = ledger.instrument_by_label(r).map(|i| i.id);
|
||||
match (by_id, by_label) {
|
||||
(Some(i), Some(l)) if i != l => Err(format!(
|
||||
"payment method {r:?} is ambiguous: it is method {i}'s id and method {l}'s label; pass the id"
|
||||
)),
|
||||
(Some(i), _) | (None, Some(i)) => Ok(i),
|
||||
(None, None) => Err(format!(
|
||||
"unknown payment method {r:?}: use an id or label from books.instruments (books.add_instrument adds one)"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The decision `books.set_vendor_1099` was given, or none when it
|
||||
/// changes only the W-9 flag (INVARIANTS 1-3).
|
||||
pub(super) fn decision_in(p: &SetVendor1099Params) -> Result<Option<Dated1099>, String> {
|
||||
let Some(value) = p.form_1099.as_deref() else {
|
||||
if p.reason_kind.is_some() || p.reason.is_some() || p.effective.is_some() {
|
||||
return Err(
|
||||
"reason_kind, reason and effective go with form_1099 (the decision they describe)"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if p.tin_on_file.is_none() {
|
||||
return Err(
|
||||
"nothing to change: pass form_1099 (the decision, with decided_by) or tin_on_file"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
return Ok(None);
|
||||
};
|
||||
let value = value.trim().to_ascii_lowercase();
|
||||
let decision = match value.as_str() {
|
||||
"not_reportable" => {
|
||||
let kind = p.reason_kind.as_deref().ok_or(
|
||||
"not_reportable needs reason_kind: corporation, tax_exempt, united_states, state, foreign_government, foreign_person, merchandise, employee, or other",
|
||||
)?;
|
||||
let kind = ExemptReason::parse(kind).ok_or_else(|| {
|
||||
format!(
|
||||
"reason_kind {kind:?}: expected corporation, tax_exempt, united_states, state, foreign_government, foreign_person, merchandise, employee, or other (paid by card is not a reason: card payments are excluded payment by payment)"
|
||||
)
|
||||
})?;
|
||||
let reason = p.reason.as_deref().filter(|r| !r.trim().is_empty()).ok_or(
|
||||
"not_reportable needs reason: the facts, in words (\"a corporation by name\")",
|
||||
)?;
|
||||
if kind == ExemptReason::Other
|
||||
&& p.citation.as_deref().is_none_or(|c| c.trim().is_empty())
|
||||
{
|
||||
return Err(
|
||||
"reason_kind other needs a citation: the rule or document that says no 1099 is due"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
Decision1099::not_reportable(kind, reason)?
|
||||
}
|
||||
"none" | "" | "no" | "no_1099" => {
|
||||
return Err(format!(
|
||||
"form_1099 {value:?}: say not_reportable with its reason_kind and reason (a corporation, the United States or a state, a payee selling only merchandise, ...), or undetermined. Paid by card is not a reason: card payments are excluded payment by payment"
|
||||
));
|
||||
}
|
||||
"undetermined" => Decision1099::Undetermined,
|
||||
other => Decision1099::Files(Form1099::parse(other).ok_or_else(|| {
|
||||
format!("form_1099 {other:?}: expected nec, misc, not_reportable, or undetermined")
|
||||
})?),
|
||||
};
|
||||
if !matches!(decision, Decision1099::NotReportable { .. })
|
||||
&& (p.reason_kind.is_some() || p.reason.is_some())
|
||||
{
|
||||
return Err(format!(
|
||||
"reason_kind and reason go with not_reportable, not {value}"
|
||||
));
|
||||
}
|
||||
let decided_by = p
|
||||
.decided_by
|
||||
.as_deref()
|
||||
.ok_or("decided_by is required with form_1099: who decided (a person's name)")?;
|
||||
let source = ledger_core::settings::Source {
|
||||
decided_by: decided_by.trim().to_string(),
|
||||
decided_on: match &p.decided_on {
|
||||
Some(d) => parse_date(d)?,
|
||||
None => today(),
|
||||
},
|
||||
// A blank citation or note is none.
|
||||
citation: p
|
||||
.citation
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|c| !c.is_empty())
|
||||
.map(str::to_string),
|
||||
note: p
|
||||
.note
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|n| !n.is_empty())
|
||||
.map(str::to_string),
|
||||
};
|
||||
Ok(Some(Dated1099 {
|
||||
effective: p.effective.as_deref().map(parse_date).transpose()?,
|
||||
decision,
|
||||
source: Some(source),
|
||||
}))
|
||||
}
|
||||
|
||||
/// What a change reaching `payments` (entry, date) warns of (INVARIANT 4):
|
||||
/// one line per calendar year with a payment in a closed period or whose
|
||||
/// 1099s were due by `today`.
|
||||
pub(super) fn filed_year_warnings(
|
||||
ledger: &Ledger,
|
||||
what: &str,
|
||||
payments: &[(ledger_core::EntryId, NaiveDate)],
|
||||
today: NaiveDate,
|
||||
) -> Vec<String> {
|
||||
let mut years: std::collections::BTreeMap<i32, (Vec<ledger_core::EntryId>, bool)> =
|
||||
Default::default();
|
||||
for &(e, d) in payments {
|
||||
let year = chrono::Datelike::year(&d);
|
||||
let closed = ledger.closed_period_containing(d).is_some();
|
||||
let due = NaiveDate::from_ymd_opt(year + 1, 1, 31).is_none_or(|due| today > due);
|
||||
if closed || due {
|
||||
let y = years.entry(year).or_default();
|
||||
y.0.push(e);
|
||||
y.1 |= closed;
|
||||
}
|
||||
}
|
||||
years
|
||||
.into_iter()
|
||||
.map(|(year, (mut ids, closed))| {
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
let ids = ids.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(", ");
|
||||
let when = if closed {
|
||||
", in a closed period".to_string()
|
||||
} else {
|
||||
format!(", a year whose 1099s were due January 31, {}", year + 1)
|
||||
};
|
||||
format!(
|
||||
"This changes {what} for {year} (entries {ids}){when}: if a {year} 1099 was filed, a corrected one may be needed. Tell the human and the CPA."
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `books.set_vendor_1099`'s "changed".
|
||||
pub(super) fn applied_key(a: Applied) -> &'static str {
|
||||
match a {
|
||||
Applied::Set => "set",
|
||||
Applied::Removed => "removed",
|
||||
Applied::Unchanged => "unchanged",
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_router(router = instruments_router, vis = "pub(super)")]
|
||||
impl BooksServer {
|
||||
#[tool(
|
||||
name = "books.add_instrument",
|
||||
title = "Add a payment method",
|
||||
description = "Record a way the company pays, once, so the 1099 worksheet tells card payments (Form 1099-K's, never the company's 1099) from checks. kind: bank (company checks, ACH, wires, bill-pay); business_card or member_card (a company or a member's card the payee charged); member_cash_or_check (a member's own cash, check, ACH, Zelle, or friends-and-family transfer, even card-funded); platform (paid to the payee as a seller through PayPal or Venmo goods and services; never ACH, bill-pay, Zelle or friends-and-family). account: what it pays through (the bank, the card's liability, Due to Members for a member's own). last4 only. Then tag payments with books.set_entry_instrument.",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = false,
|
||||
open_world_hint = false
|
||||
)
|
||||
)]
|
||||
async fn add_instrument(
|
||||
&self,
|
||||
params: Parameters<AddInstrumentParams>,
|
||||
) -> Result<Json<AddInstrumentOut>, String> {
|
||||
let p = params.0;
|
||||
let mut store = self.lock()?;
|
||||
let kind = InstrumentKind::parse(&p.kind).ok_or_else(|| {
|
||||
format!(
|
||||
"kind {:?}: expected bank, business_card, member_card, member_cash_or_check, or platform",
|
||||
p.kind
|
||||
)
|
||||
})?;
|
||||
let account = resolve_account(&store, &p.account)?;
|
||||
let owner = p
|
||||
.owner_party
|
||||
.as_deref()
|
||||
.map(|r| resolve_party(&store, r))
|
||||
.transpose()?;
|
||||
let last4 = p.last4.as_deref().map(str::trim);
|
||||
let id = store
|
||||
.add_instrument(kind, &p.label, last4, owner, account)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let settings = self.settings();
|
||||
let code = |a: AccountId| {
|
||||
store
|
||||
.ledger()
|
||||
.chart()
|
||||
.get(a)
|
||||
.map(|a| a.code.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let mut warnings = Vec::new();
|
||||
if kind == InstrumentKind::BusinessCard && settings.roles.financing.contains(&code(account))
|
||||
{
|
||||
warnings.push(format!(
|
||||
"account {} is one of the books' financing accounts: a company card's liability usually isn't, and while it is, its payments with no method recorded stay undetermined. Check the financing accounts (books.settings).",
|
||||
code(account)
|
||||
));
|
||||
}
|
||||
if kind.is_card_or_network() && settings.roles.cash.contains(&code(account)) {
|
||||
warnings.push(format!(
|
||||
"account {} is a cash account: only the payments tagged with this method are excluded as card payments; untagged ones from it still count (presumed check, ACH or transfer).",
|
||||
code(account)
|
||||
));
|
||||
}
|
||||
let members = matches!(
|
||||
kind,
|
||||
InstrumentKind::MemberCard | InstrumentKind::MemberCashOrCheck
|
||||
);
|
||||
if members
|
||||
&& !settings.roles.financing.contains(&code(account))
|
||||
&& !settings.roles.cash.contains(&code(account))
|
||||
{
|
||||
warnings.push(format!(
|
||||
"account {} is not one of the books' financing accounts: the 1099 worksheet still holds what members paid through it undetermined until each payment's method is recorded, but name it in books.financing_accounts (books.set_setting) so the books read it as what members paid for the company.",
|
||||
code(account)
|
||||
));
|
||||
}
|
||||
let i = store.ledger().get_instrument(id).expect("just added");
|
||||
Ok(Json(AddInstrumentOut {
|
||||
instrument: instrument_out(&store, i),
|
||||
warnings,
|
||||
}))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "books.instruments",
|
||||
title = "Payment methods",
|
||||
description = "The company's payment methods (books.add_instrument), each with its kind, whether its payments are card or payment-network payments (excluded from the company's 1099s), its last four, and the account it pays through. Tag an entry with one by id or label (books.set_entry_instrument, books.draft_entry instrument).",
|
||||
annotations(read_only_hint = true, open_world_hint = false)
|
||||
)]
|
||||
async fn instruments(
|
||||
&self,
|
||||
_params: Parameters<InstrumentsParams>,
|
||||
) -> Result<Json<InstrumentsOut>, String> {
|
||||
let store = self.lock()?;
|
||||
Ok(Json(InstrumentsOut {
|
||||
instruments: store
|
||||
.ledger()
|
||||
.instruments()
|
||||
.map(|i| instrument_out(&store, i))
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "books.set_entry_instrument",
|
||||
title = "Tag an entry's payment method",
|
||||
description = "Tag an entry (draft or posted) with how it was paid (a payment method's id or label, books.instruments), or omit instrument to clear it, so the 1099 worksheet can classify it: a card or payment network's payment is excluded (Form 1099-K's), a bank or a member's own cash or check counts. Needed above all for what members paid for the company (their card or their check: undetermined until tagged). Reporting metadata only: amounts never change. An entry and its reversals are tagged together. Warns when it changes a year whose 1099s are due or a closed period.",
|
||||
annotations(
|
||||
read_only_hint = false,
|
||||
destructive_hint = false,
|
||||
idempotent_hint = true,
|
||||
open_world_hint = false
|
||||
)
|
||||
)]
|
||||
async fn set_entry_instrument(
|
||||
&self,
|
||||
params: Parameters<SetEntryInstrumentParams>,
|
||||
) -> Result<Json<SetEntryInstrumentOut>, String> {
|
||||
let p = params.0;
|
||||
let (changed, warnings) = {
|
||||
let mut store = self.lock()?;
|
||||
let instrument = p
|
||||
.instrument
|
||||
.as_deref()
|
||||
.map(|r| resolve_instrument(&store, r))
|
||||
.transpose()?;
|
||||
let ledger = store.ledger();
|
||||
// The chain's posted payments to a vendor: what the change
|
||||
// reaches on its 1099s.
|
||||
let reached: Vec<(ledger_core::EntryId, NaiveDate)> = ledger
|
||||
.reversal_chain(p.entry_id)
|
||||
.into_iter()
|
||||
.filter_map(|e| ledger.get_entry(e))
|
||||
.filter(|e| {
|
||||
e.status() == ledger_core::EntryStatus::Posted
|
||||
&& ledger.entry_vendor(e.id()).is_some()
|
||||
&& ledger.entry_instrument(e.id()) != instrument
|
||||
})
|
||||
.map(|e| (e.id(), e.date()))
|
||||
.collect();
|
||||
let warnings =
|
||||
filed_year_warnings(ledger, "how a vendor's payment counts", &reached, today());
|
||||
let changed = store
|
||||
.set_entry_instrument(p.entry_id, instrument)
|
||||
.map_err(|e| e.to_string())?;
|
||||
(changed, if changed { warnings } else { Vec::new() })
|
||||
};
|
||||
let entry = self
|
||||
.get_entry(Parameters(GetEntryParams {
|
||||
entry_id: p.entry_id,
|
||||
}))
|
||||
.await?
|
||||
.0;
|
||||
Ok(Json(SetEntryInstrumentOut {
|
||||
entry,
|
||||
changed,
|
||||
warnings,
|
||||
}))
|
||||
}
|
||||
}
|
||||
+180
-19
@@ -367,14 +367,47 @@ pub struct VendorOut {
|
||||
pub id: u64,
|
||||
/// Name.
|
||||
pub name: String,
|
||||
/// "1099-NEC", "1099-MISC", or absent.
|
||||
/// "1099-NEC" or "1099-MISC" when its 1099 decision in force today
|
||||
/// files one; absent otherwise.
|
||||
pub form_1099: Option<String>,
|
||||
/// Its 1099 decision in force today. value "undetermined" with no
|
||||
/// decided_by: nobody has decided.
|
||||
pub decision_1099: Decision1099Out,
|
||||
/// Every 1099 decision recorded, in date order (the one from the
|
||||
/// beginning first): more than one when its status changed on a date.
|
||||
pub decisions_1099: Vec<Decision1099Out>,
|
||||
/// W-9 on file.
|
||||
pub tin_on_file: bool,
|
||||
/// The party the vendor record belongs to.
|
||||
pub party_id: Option<u64>,
|
||||
}
|
||||
|
||||
/// A vendor's 1099 decision.
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
pub struct Decision1099Out {
|
||||
/// "nec", "misc", "not_reportable" or "undetermined".
|
||||
pub value: String,
|
||||
/// With not_reportable: the kind of payee (corporation, tax_exempt,
|
||||
/// united_states, state, foreign_government, foreign_person,
|
||||
/// merchandise, employee, other).
|
||||
pub reason_kind: Option<String>,
|
||||
/// With not_reportable: the facts, in words.
|
||||
pub reason: Option<String>,
|
||||
/// With not_reportable: the rule that exempts that kind of payee.
|
||||
pub authority: Option<String>,
|
||||
/// The day it takes effect, YYYY-MM-DD; absent: from the beginning.
|
||||
pub effective: Option<String>,
|
||||
/// Who decided; absent for a vendor nobody decided, or a decision
|
||||
/// recorded before decisions were kept.
|
||||
pub decided_by: Option<String>,
|
||||
/// When they decided, YYYY-MM-DD.
|
||||
pub decided_on: Option<String>,
|
||||
/// What it rests on, if anything.
|
||||
pub citation: Option<String>,
|
||||
/// Anything kept with it.
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of `books.list_vendors`.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct ListVendorsOut {
|
||||
@@ -387,12 +420,54 @@ pub struct ListVendorsOut {
|
||||
pub struct SetVendor1099Params {
|
||||
/// Vendor id or name.
|
||||
pub vendor: String,
|
||||
/// "nec" (contractors, attorneys), "misc" (rent, royalties, other), or
|
||||
/// "none" (corporations, most merchants).
|
||||
pub form_1099: String,
|
||||
/// A W-9 is on file (the TIN itself is never stored).
|
||||
#[serde(default)]
|
||||
pub tin_on_file: bool,
|
||||
/// The decision: "nec" (1099-NEC: contractors, freelancers,
|
||||
/// attorneys), "misc" (1099-MISC: rent, royalties, prizes),
|
||||
/// "not_reportable" (with reason_kind and reason), or "undetermined".
|
||||
/// Omit to change only tin_on_file.
|
||||
pub form_1099: Option<String>,
|
||||
/// With not_reportable: the kind of payee no 1099 is required for:
|
||||
/// corporation, tax_exempt, united_states, state, foreign_government,
|
||||
/// foreign_person, merchandise (sells only merchandise), employee, or
|
||||
/// other (give a citation). Never corporation for a payee paid for
|
||||
/// legal services or for medical and health care (a law firm or a
|
||||
/// medical practice, often a "P.C."): those payments stay reportable
|
||||
/// even to a corporation (Treas. Reg. §1.6041-3(p)(1)).
|
||||
pub reason_kind: Option<String>,
|
||||
/// With not_reportable: the facts, in words ("a corporation by name",
|
||||
/// "Delaware public benefit corporation, from the public record").
|
||||
pub reason: Option<String>,
|
||||
/// The day the decision takes effect, YYYY-MM-DD: only when the
|
||||
/// payee's status changed (an LLC's corporate election takes effect
|
||||
/// on its Form 8832 date; a new W-9 showing a new classification, on
|
||||
/// the form's date). Omit for its status from the beginning.
|
||||
pub effective: Option<String>,
|
||||
/// A W-9 is on file (the TIN itself is never stored). Omit to keep it
|
||||
/// as it is.
|
||||
pub tin_on_file: Option<bool>,
|
||||
/// Who decided (a person's name): required with form_1099.
|
||||
pub decided_by: Option<String>,
|
||||
/// When they decided, YYYY-MM-DD. Default: today.
|
||||
pub decided_on: Option<String>,
|
||||
/// The rule or document it rests on ("Treas. Reg. §1.6041-3(p)(1)", a
|
||||
/// W-9 dated 2026-03-02).
|
||||
pub citation: Option<String>,
|
||||
/// Anything else worth keeping with it.
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of `books.set_vendor_1099`.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct SetVendor1099Out {
|
||||
/// The vendor as it stands now.
|
||||
#[serde(flatten)]
|
||||
pub vendor: VendorOut,
|
||||
/// What the decision did: "set", "removed" (a dated decision taken
|
||||
/// out: the one before it applies again), or "unchanged" (already so;
|
||||
/// nothing journaled but a changed W-9 flag).
|
||||
pub changed: String,
|
||||
/// Payments the change reaches in a closed period, or in a year whose
|
||||
/// 1099s were due: tell the human (a filed 1099 may need correcting).
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Parameters for `books.set_entry_vendor`.
|
||||
@@ -421,6 +496,20 @@ pub struct Vendor1099Params {
|
||||
/// with include_tins), to save with its download command.
|
||||
#[serde(default)]
|
||||
pub csv: bool,
|
||||
/// The cash account codes payments are counted out of, e.g.
|
||||
/// `["1000"]`. Defaults to `[books] cash_accounts`, as the CPA
|
||||
/// package's do: one or the other is required.
|
||||
#[serde(default)]
|
||||
pub cash_accounts: Vec<String>,
|
||||
/// The financing liability codes (what the members advanced, loans)
|
||||
/// payments are counted out of. Defaults to `[books]
|
||||
/// financing_accounts`, as the CPA package's do.
|
||||
#[serde(default)]
|
||||
pub financing_accounts: Vec<String>,
|
||||
/// Count only the payments dated on or before this day of `year`
|
||||
/// (YYYY-MM-DD): the worksheet as of a day, the one a CPA package as
|
||||
/// of that day carries. Default: the whole year.
|
||||
pub through: Option<String>,
|
||||
}
|
||||
|
||||
/// One vendor's year.
|
||||
@@ -430,13 +519,37 @@ pub struct Vendor1099LineOut {
|
||||
pub vendor_id: u64,
|
||||
/// Name.
|
||||
pub name: String,
|
||||
/// "1099-NEC", "1099-MISC", or absent.
|
||||
/// "1099-NEC" or "1099-MISC" when its decision at the year's end files
|
||||
/// one; absent otherwise.
|
||||
pub form_1099: Option<String>,
|
||||
/// Its 1099 decision at the year's end.
|
||||
pub decision_1099: Decision1099Out,
|
||||
/// Every decision in force during the year, in date order (more than
|
||||
/// one when its status changed in the year; none when never decided).
|
||||
pub decisions_in_year: Vec<Decision1099Out>,
|
||||
/// W-9 on file.
|
||||
pub tin_on_file: bool,
|
||||
/// Cash paid in the year, decimal string.
|
||||
/// What counts toward its 1099, decimal string: paid by check, ACH,
|
||||
/// transfer or cash (net of refunds and reversals) while it wasn't
|
||||
/// decided not reportable.
|
||||
pub paid: String,
|
||||
/// Marked for a 1099 and paid at least the threshold.
|
||||
/// Paid by card or payment network: reported by the processor on Form
|
||||
/// 1099-K, never on the company's 1099, decimal string.
|
||||
pub excluded_card: String,
|
||||
/// Paid through a financing account (or a member's or a payment
|
||||
/// platform's) with no payment method recorded: counts or not once it
|
||||
/// is (books.set_entry_instrument), decimal string.
|
||||
pub undetermined: String,
|
||||
/// Paid while it was decided not reportable, decimal string.
|
||||
pub not_reportable: String,
|
||||
/// "reportable" (file a 1099), "under_threshold", "not_reportable"
|
||||
/// (its decision says why), or "undetermined" (a decision or a payment
|
||||
/// method is needed first).
|
||||
pub status: String,
|
||||
/// Some of its payments that could count were made while its decision
|
||||
/// was undetermined: ask the human and record one.
|
||||
pub decision_needed: bool,
|
||||
/// File a 1099: status reportable.
|
||||
pub reportable: bool,
|
||||
/// The vendor's party.
|
||||
pub party_id: Option<u64>,
|
||||
@@ -452,6 +565,23 @@ pub struct Vendor1099LineOut {
|
||||
pub tin_note: Option<String>,
|
||||
}
|
||||
|
||||
/// One payment on the 1099 worksheet's lists.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct Payment1099Out {
|
||||
/// The entry.
|
||||
pub entry_id: u64,
|
||||
/// Its date, YYYY-MM-DD.
|
||||
pub date: String,
|
||||
/// The vendor it paid.
|
||||
pub vendor_id: u64,
|
||||
/// The vendor's name.
|
||||
pub vendor: String,
|
||||
/// The vendor's part the list is about, decimal string.
|
||||
pub amount: String,
|
||||
/// The payment method it is tagged with (label), if any.
|
||||
pub instrument: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of `books.vendor_1099`.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct Vendor1099Out {
|
||||
@@ -459,10 +589,26 @@ pub struct Vendor1099Out {
|
||||
pub year: i32,
|
||||
/// Threshold applied, decimal string.
|
||||
pub threshold: String,
|
||||
/// Vendors marked for a 1099 or paid in the year.
|
||||
/// Vendors decided for a 1099 during the year, or with any payment in
|
||||
/// it.
|
||||
pub vendors: Vec<Vendor1099LineOut>,
|
||||
/// Reportable vendors without a W-9 on file: request one.
|
||||
/// Vendors a 1099 is or may be due for without a W-9 on file
|
||||
/// ("(may be reportable)" after the undetermined): request one.
|
||||
pub missing_w9: Vec<String>,
|
||||
/// Payments by card or payment network, excluded (Form 1099-K's).
|
||||
pub excluded_card_payments: Vec<Payment1099Out>,
|
||||
/// Payments with no payment method recorded where it decides whether
|
||||
/// they count: record each with books.set_entry_instrument.
|
||||
pub undetermined_payments: Vec<Payment1099Out>,
|
||||
/// Money received from a vendor for something else (a sale, its
|
||||
/// payment of an invoice, a disposal, a loan), not netted against its
|
||||
/// payments, with the reversals of each.
|
||||
pub other_receipts: Vec<Payment1099Out>,
|
||||
/// Posted entries in the year that paid money out with no vendor tag.
|
||||
pub untagged_entries: Vec<u64>,
|
||||
/// Entries naming several parties whose shares don't resolve (each
|
||||
/// counted in full toward its vendor).
|
||||
pub unresolved: Vec<u64>,
|
||||
/// Caveats for the human and CPA.
|
||||
pub notes: Vec<String>,
|
||||
/// With include_tins: this output holds full TINs, for the CPA.
|
||||
@@ -1208,6 +1354,9 @@ pub struct DraftEntryParams {
|
||||
pub description: String,
|
||||
/// The vendor this entry pays (id or name), for 1099 tracking.
|
||||
pub vendor: Option<String>,
|
||||
/// How it was paid: a payment method's id or label (books.instruments),
|
||||
/// for 1099 tracking.
|
||||
pub instrument: Option<String>,
|
||||
/// The party this entry is with (id or name; see `books.parties`). A
|
||||
/// party with a vendor record also tags the entry's vendor; given with
|
||||
/// `vendor`, both must name the same vendor.
|
||||
@@ -1749,6 +1898,10 @@ pub struct GetEntryOut {
|
||||
pub receipts: Vec<PreviewReceipt>,
|
||||
/// The vendor the entry is tagged with, for 1099 tracking.
|
||||
pub vendor: Option<String>,
|
||||
/// The payment method it is tagged with (label), for 1099 tracking.
|
||||
pub instrument: Option<String>,
|
||||
/// That payment method's id.
|
||||
pub instrument_id: Option<u64>,
|
||||
/// The id of the party the entry is tagged with (its counterparty),
|
||||
/// when it names exactly one.
|
||||
pub party_id: Option<u64>,
|
||||
@@ -3449,8 +3602,10 @@ pub struct ListPartiesOut {
|
||||
/// books' accounting method.)
|
||||
///
|
||||
/// Money is what left or reached the `[books] cash_accounts` (every asset
|
||||
/// account but receivables when none are set), the cards (liabilities
|
||||
/// whose names say "card", not "gift card"), and the `[books]
|
||||
/// account but receivables when none are set), the cards (the
|
||||
/// business-card payment methods' liabilities, `books.instruments`; while
|
||||
/// none is recorded, liabilities whose names say "card", not "gift
|
||||
/// card"), and the `[books]
|
||||
/// financing_accounts` (what the members
|
||||
/// advanced, loans): a card charge or a member paying for the company is
|
||||
/// paid to the payee then; cash that pays one of these down is paid to
|
||||
@@ -3506,13 +3661,19 @@ pub struct PartyTotalsOut {
|
||||
/// Still owed by it now: its open invoices' balances (every year),
|
||||
/// decimal string.
|
||||
pub receivable: String,
|
||||
/// For a party with a vendor record: the cash `books.vendor_1099`
|
||||
/// counts as paid to that vendor in the year (asset accounts only, so
|
||||
/// card payments drop out), decimal string. Absent otherwise.
|
||||
/// For a party with a vendor record: what `books.vendor_1099` counts
|
||||
/// toward that vendor's 1099 in the year (out of the cash and
|
||||
/// financing accounts; card and payment-network payments excluded,
|
||||
/// payments with no method recorded on a financing account left
|
||||
/// undetermined), decimal string. Absent otherwise, or when the
|
||||
/// worksheet can't be made (no cash accounts named).
|
||||
pub amount_1099: Option<String>,
|
||||
/// The information return its vendor record gets ("1099-NEC",
|
||||
/// "1099-MISC"), if any.
|
||||
/// The information return its vendor record's decision in force today
|
||||
/// files ("1099-NEC", "1099-MISC"), if any.
|
||||
pub form_1099: Option<String>,
|
||||
/// Its vendor record's 1099 decision in force today ("nec", "misc",
|
||||
/// "not_reportable", "undetermined"). Absent without a vendor record.
|
||||
pub decision_1099: Option<String>,
|
||||
/// Gross pay on payroll lines naming it, decimal string.
|
||||
pub gross_pay: String,
|
||||
/// Net pay on payroll lines naming it, decimal string.
|
||||
|
||||
Reference in new issue
Block a user