JournalEntryにtax_amount_sourceという項目を追加した。これは税区分(tax_type)とは別に、「そのtax_amountという数字がどうやって決まったのか」を記録するための項目。ここに落ち着くまでに、一度違う方向に進みかけている。
そもそもの出発点:deemedをどう扱うか
Solerは内部的にnet_amountとtax_amountを分けて保存する。ただ免税事業者にはこの分解を意識させたくないし、将来「免税事業者が課税事業者に変わったら消費税をどれくらい払うことになるか」をシミュレーションする機能も作りたいと思っている。そのために、deemed_taxable_sales_10やdeemed_taxable_purchases_10のような「見なし」区分をtax_typeの一種として持たせていた。
このdeemed_*をtax_typeにこのまま持たせ続けるべきか、というのが最初の論点だった。考えているうちに気づいたのは、deemedは「税区分そのもの」というより「その年度設定や事業者区分を前提に、どう解釈するか」という文脈依存の情報だということ。通常のtaxable_sales_10とdeemed_taxable_sales_10を並列の固定tax_typeとして持つと、本来別の軸である「分類」と「解釈」が混ざってしまう。
一度はtax_type_sourceに進みかけた
そこで、「tax_typeの決定理由を別に持てばいいのでは」と考えた。tax_type_sourceのようなものを設けて、ユーザーが明示したのか、デフォルトで入ったのか、内部補完したのかを区別しようとした案。
一見よさそうだったが、途中で違和感が出た。実際に曖昧になっていたのはtax_typeではなく、「このtax_amountがどう作られたか」の方だったからだ。
そもそも自分の頭の中では、税額は「みなし」か「本当」かの2択でしかなかった。免税事業者の見なし10%か、そうでない本当の税額か、という区別さえ付けばいいと思っていて、「本当」の内側にどんな根拠があるかまでは考えていなかった。でもよく考えると、同じ「本当」の中にも
- 税込総額と
tax_typeから、内部的に按分計算して求めた - ユーザーが税額を明示的に入力した
tax_typeはあるが税額そのものは渡されておらず、内部の既定値(0円)に任せた
という違う出どころが混ざっていて、これを区別せずに保存すると、あとから「この税額は本当にユーザーが入力した通りなのか、それとも自動で埋まった値なのか」がトレースできなくなる。ここまで気づいて、tax_typeの由来ではなくtax_amountの算出根拠を持つ方が筋が良いと考え、tax_type_sourceではなくtax_amount_sourceという形に切り替えた。
3つの区分
user_input: ユーザーがtax_amountを明示指定したdefaulted:tax_typeはあるがtax_amountを渡しておらず、内部の既定値(0円)で補完したcomputed_from_gross:gross_amountとtax_typeからnet_amount・tax_amountを計算した
まずは小さく始めることにして、この3つだけにした。この整理の良かったところは、課税事業者の通常入力にとても素直に対応できる点。税込入力ならcomputed_from_gross、税額を明示したならuser_input、税額を渡さず0円で補完されたならdefaultedと表現できる。
TransactionRegistrar::prepareJournalEntry()の中で、gross_amountが渡された行は問答無用でcomputed_from_grossになる。gross_amountを経由しない、net_amount/tax_amountを直接渡すパスだけ、withTaxAmountSource()というヘルパーでuser_inputかdefaultedかを振り分ける。
$entry['tax_amount_source'] = array_key_exists('tax_amount', $entry)
? JournalEntry::TAX_AMOUNT_SOURCE_USER_INPUT
: JournalEntry::TAX_AMOUNT_SOURCE_DEFAULTED;
tax_typeが渡されていない行(そもそも消費税を扱わない行)ではtax_amount_source自体を持たせない。すべての行に無理やり値を入れると、「値があること」自体の意味が薄れてしまうため。
家事按分との組み合わせ
按分が絡む行(事業分・家事分に分割される行)も、分割前にcomputed_from_grossを確定させてから、事業分・家事分それぞれの新しい仕訳データに同じtax_amount_sourceを持たせている。按分の有無で根拠の意味が変わるわけではないので、単純にコピーするだけで済んだ。
スコープ外にしたこと
免税事業者の「課税だった場合のシミュレーション」のような意味は、computed_from_grossだけでは表しきれないこともはっきりした。これは算出方法ではなく、用途や前提の問題だから。その意味づけが必要になったら、is_tax_simulatedのような別軸のフラグとして持つべきで、tax_amount_sourceに混ぜ込むと後で両方の軸での場合分けが必要になったときに破綻する。
振り返ると、tax_typeは「分類」、tax_amount_sourceは「算出根拠」と役割を分けたのが大きかった。ここを分けたことで、税区分の意味と内部計算の履歴が、ようやく整理され始めた感じがする。