j-law-ruby 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/README.md +109 -0
- data/Rakefile +87 -0
- data/ext/j_law_ruby/extconf.rb +34 -0
- data/lib/j_law_ruby/build_support.rb +129 -0
- data/lib/j_law_ruby/c_ffi.rb +662 -0
- data/lib/j_law_ruby.rb +532 -0
- data/rake_support/vendor_rust.rb +51 -0
- data/test/test_c_ffi_adapter.rb +46 -0
- data/test/test_consumption_tax.rb +66 -0
- data/test/test_gemspec.rb +82 -0
- data/test/test_income_tax.rb +77 -0
- data/test/test_income_tax_deductions.rb +82 -0
- data/test/test_real_estate.rb +98 -0
- data/test/test_stamp_tax.rb +68 -0
- data/test/test_withholding_tax.rb +65 -0
- data/vendor/rust/Cargo.lock +235 -0
- data/vendor/rust/Cargo.toml +12 -0
- data/vendor/rust/crates/j-law-c-ffi/Cargo.toml +20 -0
- data/vendor/rust/crates/j-law-c-ffi/j_law_c_ffi.h +493 -0
- data/vendor/rust/crates/j-law-c-ffi/src/lib.rs +1553 -0
- data/vendor/rust/crates/j-law-core/Cargo.toml +18 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/calculator.rs +216 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/context.rs +29 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/mod.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/params.rs +24 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/policy.rs +34 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/assessment.rs +76 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/calculator.rs +222 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/context.rs +79 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/calculator.rs +167 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/context.rs +172 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/expense.rs +465 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/mod.rs +20 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/params.rs +205 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/personal.rs +324 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/types.rs +61 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/mod.rs +24 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/params.rs +109 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/policy.rs +103 -0
- data/vendor/rust/crates/j-law-core/src/domains/mod.rs +5 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/calculator.rs +197 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/context.rs +48 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/mod.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/params.rs +43 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/policy.rs +40 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/calculator.rs +321 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/context.rs +408 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/mod.rs +12 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/params.rs +190 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/policy.rs +105 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/calculator.rs +247 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/context.rs +167 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/mod.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/params.rs +80 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/policy.rs +49 -0
- data/vendor/rust/crates/j-law-core/src/error.rs +171 -0
- data/vendor/rust/crates/j-law-core/src/lib.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/types/amount.rs +232 -0
- data/vendor/rust/crates/j-law-core/src/types/citation.rs +82 -0
- data/vendor/rust/crates/j-law-core/src/types/date.rs +280 -0
- data/vendor/rust/crates/j-law-core/src/types/mod.rs +11 -0
- data/vendor/rust/crates/j-law-core/src/types/rate.rs +219 -0
- data/vendor/rust/crates/j-law-core/src/types/rounding.rs +81 -0
- data/vendor/rust/crates/j-law-registry/Cargo.toml +15 -0
- data/vendor/rust/crates/j-law-registry/data/consumption_tax/consumption_tax.json +70 -0
- data/vendor/rust/crates/j-law-registry/data/income_tax/deductions.json +327 -0
- data/vendor/rust/crates/j-law-registry/data/income_tax/income_tax.json +352 -0
- data/vendor/rust/crates/j-law-registry/data/real_estate/brokerage_fee.json +125 -0
- data/vendor/rust/crates/j-law-registry/data/stamp_tax/stamp_tax.json +674 -0
- data/vendor/rust/crates/j-law-registry/data/withholding_tax/withholding_tax.json +70 -0
- data/vendor/rust/crates/j-law-registry/src/consumption_tax_loader.rs +325 -0
- data/vendor/rust/crates/j-law-registry/src/consumption_tax_schema.rs +49 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_loader.rs +636 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_schema.rs +111 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_loader.rs +445 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_schema.rs +44 -0
- data/vendor/rust/crates/j-law-registry/src/lib.rs +20 -0
- data/vendor/rust/crates/j-law-registry/src/loader.rs +221 -0
- data/vendor/rust/crates/j-law-registry/src/schema.rs +73 -0
- data/vendor/rust/crates/j-law-registry/src/stamp_tax_loader.rs +374 -0
- data/vendor/rust/crates/j-law-registry/src/stamp_tax_schema.rs +72 -0
- data/vendor/rust/crates/j-law-registry/src/validator.rs +204 -0
- data/vendor/rust/crates/j-law-registry/src/withholding_tax_loader.rs +310 -0
- data/vendor/rust/crates/j-law-registry/src/withholding_tax_schema.rs +61 -0
- metadata +148 -0
data/lib/j_law_ruby.rb
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require_relative "j_law_ruby/build_support"
|
|
5
|
+
require_relative "j_law_ruby/c_ffi"
|
|
6
|
+
|
|
7
|
+
# 日本の法令に基づく各種計算を提供するモジュール。
|
|
8
|
+
#
|
|
9
|
+
# `j-law-c-ffi` の C FFI を ffi gem 経由でラップし、
|
|
10
|
+
# Ruby Date オブジェクトを受け取るインターフェースを提供する。
|
|
11
|
+
module JLawRuby
|
|
12
|
+
# ── 消費税 ──────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
module ConsumptionTax
|
|
15
|
+
# 消費税の計算結果。
|
|
16
|
+
class ConsumptionTaxResult
|
|
17
|
+
attr_reader :tax_amount, :amount_with_tax, :amount_without_tax,
|
|
18
|
+
:applied_rate_numer, :applied_rate_denom
|
|
19
|
+
|
|
20
|
+
def initialize(r)
|
|
21
|
+
@tax_amount = r.tax_amount
|
|
22
|
+
@amount_with_tax = r.amount_with_tax
|
|
23
|
+
@amount_without_tax = r.amount_without_tax
|
|
24
|
+
@applied_rate_numer = r.applied_rate_numer
|
|
25
|
+
@applied_rate_denom = r.applied_rate_denom
|
|
26
|
+
@is_reduced_rate = r.is_reduced_rate
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# 軽減税率が適用されたか。
|
|
30
|
+
def is_reduced_rate?
|
|
31
|
+
@is_reduced_rate
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def inspect
|
|
35
|
+
"#<JLawRuby::ConsumptionTax::ConsumptionTaxResult " \
|
|
36
|
+
"tax_amount=#{@tax_amount} " \
|
|
37
|
+
"amount_with_tax=#{@amount_with_tax} " \
|
|
38
|
+
"amount_without_tax=#{@amount_without_tax} " \
|
|
39
|
+
"applied_rate=#{@applied_rate_numer}/#{@applied_rate_denom} " \
|
|
40
|
+
"is_reduced_rate=#{@is_reduced_rate}>"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
alias to_s inspect
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 消費税法第29条に基づく消費税額を計算する。
|
|
47
|
+
#
|
|
48
|
+
# @param amount [Integer] 課税標準額(税抜き・円)
|
|
49
|
+
# @param date [Date] 基準日
|
|
50
|
+
# @param is_reduced_rate [true, false] 軽減税率フラグ
|
|
51
|
+
# @return [ConsumptionTaxResult]
|
|
52
|
+
# @raise [TypeError] date が Date / DateTime 以外の場合
|
|
53
|
+
# @raise [RuntimeError] 計算エラーが発生した場合
|
|
54
|
+
def self.calc_consumption_tax(amount, date, is_reduced_rate = false)
|
|
55
|
+
unless date.is_a?(::Date) || date.is_a?(::DateTime)
|
|
56
|
+
raise TypeError,
|
|
57
|
+
"date には Date または DateTime を指定してください (got #{date.class})"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
r = Internal::CFFI.calc_consumption_tax(amount, date.year, date.month, date.day, is_reduced_rate)
|
|
61
|
+
ConsumptionTaxResult.new(r)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# ── 不動産(媒介報酬) ───────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
module RealEstate
|
|
68
|
+
# 媒介報酬の計算結果。
|
|
69
|
+
class BrokerageFeeResult
|
|
70
|
+
attr_reader :total_without_tax, :total_with_tax, :tax_amount, :breakdown
|
|
71
|
+
|
|
72
|
+
def initialize(r)
|
|
73
|
+
@total_without_tax = r.total_without_tax
|
|
74
|
+
@total_with_tax = r.total_with_tax
|
|
75
|
+
@tax_amount = r.tax_amount
|
|
76
|
+
@low_cost_special_applied = r.low_cost_special_applied
|
|
77
|
+
@breakdown = r.breakdown.map do |step|
|
|
78
|
+
{
|
|
79
|
+
label: step.label,
|
|
80
|
+
base_amount: step.base_amount,
|
|
81
|
+
rate_numer: step.rate_numer,
|
|
82
|
+
rate_denom: step.rate_denom,
|
|
83
|
+
result: step.result,
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 低廉な空き家特例が適用されたか。
|
|
89
|
+
def low_cost_special_applied?
|
|
90
|
+
@low_cost_special_applied
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def inspect
|
|
94
|
+
"#<JLawRuby::RealEstate::BrokerageFeeResult " \
|
|
95
|
+
"total_without_tax=#{@total_without_tax} " \
|
|
96
|
+
"total_with_tax=#{@total_with_tax} " \
|
|
97
|
+
"tax_amount=#{@tax_amount} " \
|
|
98
|
+
"low_cost_special_applied=#{@low_cost_special_applied}>"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
alias to_s inspect
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# 宅建業法第46条に基づく媒介報酬を計算する。
|
|
105
|
+
#
|
|
106
|
+
# @param price [Integer] 売買価格(円)
|
|
107
|
+
# @param date [Date] 基準日
|
|
108
|
+
# @param is_low_cost_vacant_house [true, false] 低廉な空き家特例フラグ
|
|
109
|
+
# @param is_seller [true, false] 売主側フラグ
|
|
110
|
+
# @return [BrokerageFeeResult]
|
|
111
|
+
# @raise [TypeError] date が Date / DateTime 以外の場合
|
|
112
|
+
# @raise [RuntimeError] 計算エラーが発生した場合
|
|
113
|
+
def self.calc_brokerage_fee(price, date, is_low_cost_vacant_house, is_seller)
|
|
114
|
+
unless date.is_a?(::Date) || date.is_a?(::DateTime)
|
|
115
|
+
raise TypeError,
|
|
116
|
+
"date には Date または DateTime を指定してください (got #{date.class})"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
r = Internal::CFFI.calc_brokerage_fee(
|
|
120
|
+
price, date.year, date.month, date.day,
|
|
121
|
+
is_low_cost_vacant_house, is_seller
|
|
122
|
+
)
|
|
123
|
+
BrokerageFeeResult.new(r)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# ── 所得税 ──────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
module IncomeTax
|
|
130
|
+
# 所得税の計算結果。
|
|
131
|
+
class IncomeTaxResult
|
|
132
|
+
attr_reader :base_tax, :reconstruction_tax, :total_tax, :breakdown
|
|
133
|
+
|
|
134
|
+
def initialize(r)
|
|
135
|
+
@base_tax = r.base_tax
|
|
136
|
+
@reconstruction_tax = r.reconstruction_tax
|
|
137
|
+
@total_tax = r.total_tax
|
|
138
|
+
@reconstruction_tax_applied = r.reconstruction_tax_applied
|
|
139
|
+
@breakdown = r.breakdown.map do |step|
|
|
140
|
+
{
|
|
141
|
+
label: step.label,
|
|
142
|
+
taxable_income: step.taxable_income,
|
|
143
|
+
rate_numer: step.rate_numer,
|
|
144
|
+
rate_denom: step.rate_denom,
|
|
145
|
+
deduction: step.deduction,
|
|
146
|
+
result: step.result,
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# 復興特別所得税が適用されたか。
|
|
152
|
+
def reconstruction_tax_applied?
|
|
153
|
+
@reconstruction_tax_applied
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def inspect
|
|
157
|
+
"#<JLawRuby::IncomeTax::IncomeTaxResult " \
|
|
158
|
+
"base_tax=#{@base_tax} " \
|
|
159
|
+
"reconstruction_tax=#{@reconstruction_tax} " \
|
|
160
|
+
"total_tax=#{@total_tax} " \
|
|
161
|
+
"reconstruction_tax_applied=#{@reconstruction_tax_applied}>"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
alias to_s inspect
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# 所得控除の計算結果。
|
|
168
|
+
class IncomeDeductionResult
|
|
169
|
+
attr_reader :total_income_amount, :total_deductions,
|
|
170
|
+
:taxable_income_before_truncation, :taxable_income, :breakdown
|
|
171
|
+
|
|
172
|
+
def initialize(r)
|
|
173
|
+
@total_income_amount = r.total_income_amount
|
|
174
|
+
@total_deductions = r.total_deductions
|
|
175
|
+
@taxable_income_before_truncation = r.taxable_income_before_truncation
|
|
176
|
+
@taxable_income = r.taxable_income
|
|
177
|
+
@breakdown = r.breakdown.map do |line|
|
|
178
|
+
{
|
|
179
|
+
kind: line.kind,
|
|
180
|
+
label: line.label,
|
|
181
|
+
amount: line.amount,
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def inspect
|
|
187
|
+
"#<JLawRuby::IncomeTax::IncomeDeductionResult " \
|
|
188
|
+
"total_income_amount=#{@total_income_amount} " \
|
|
189
|
+
"total_deductions=#{@total_deductions} " \
|
|
190
|
+
"taxable_income=#{@taxable_income}>"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
alias to_s inspect
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# 所得控除から所得税額までの通し計算結果。
|
|
197
|
+
class IncomeTaxAssessmentResult
|
|
198
|
+
attr_reader :deductions, :tax
|
|
199
|
+
|
|
200
|
+
def initialize(r)
|
|
201
|
+
@deductions = IncomeDeductionResult.new(r.deductions)
|
|
202
|
+
@tax = IncomeTaxResult.new(r.tax)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def inspect
|
|
206
|
+
"#<JLawRuby::IncomeTax::IncomeTaxAssessmentResult " \
|
|
207
|
+
"taxable_income=#{@deductions.taxable_income} " \
|
|
208
|
+
"total_tax=#{@tax.total_tax}>"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
alias to_s inspect
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# 所得税法第89条に基づく所得税額を計算する。
|
|
215
|
+
#
|
|
216
|
+
# @param taxable_income [Integer] 課税所得金額(円)
|
|
217
|
+
# @param date [Date] 基準日
|
|
218
|
+
# @param apply_reconstruction_tax [true, false] 復興特別所得税を適用するか
|
|
219
|
+
# @return [IncomeTaxResult]
|
|
220
|
+
# @raise [TypeError] date が Date / DateTime 以外の場合
|
|
221
|
+
# @raise [RuntimeError] 計算エラーが発生した場合
|
|
222
|
+
def self.calc_income_tax(taxable_income, date, apply_reconstruction_tax)
|
|
223
|
+
unless date.is_a?(::Date) || date.is_a?(::DateTime)
|
|
224
|
+
raise TypeError,
|
|
225
|
+
"date には Date または DateTime を指定してください (got #{date.class})"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
r = Internal::CFFI.calc_income_tax(
|
|
229
|
+
taxable_income, date.year, date.month, date.day,
|
|
230
|
+
apply_reconstruction_tax
|
|
231
|
+
)
|
|
232
|
+
IncomeTaxResult.new(r)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# 所得控除を計算し、課税所得金額までを返す。
|
|
236
|
+
def self.calc_income_deductions(
|
|
237
|
+
total_income_amount,
|
|
238
|
+
date,
|
|
239
|
+
spouse: nil,
|
|
240
|
+
dependent: {},
|
|
241
|
+
social_insurance_premium_paid: 0,
|
|
242
|
+
medical: nil,
|
|
243
|
+
life_insurance: nil,
|
|
244
|
+
donation: nil
|
|
245
|
+
)
|
|
246
|
+
unless date.is_a?(::Date) || date.is_a?(::DateTime)
|
|
247
|
+
raise TypeError,
|
|
248
|
+
"date には Date または DateTime を指定してください (got #{date.class})"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
r = Internal::CFFI.calc_income_deductions(
|
|
252
|
+
total_income_amount: total_income_amount,
|
|
253
|
+
year: date.year,
|
|
254
|
+
month: date.month,
|
|
255
|
+
day: date.day,
|
|
256
|
+
spouse: spouse,
|
|
257
|
+
dependent: dependent,
|
|
258
|
+
social_insurance_premium_paid: social_insurance_premium_paid,
|
|
259
|
+
medical: medical,
|
|
260
|
+
life_insurance: life_insurance,
|
|
261
|
+
donation: donation
|
|
262
|
+
)
|
|
263
|
+
IncomeDeductionResult.new(r)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# 所得控除から所得税額までを通しで計算する。
|
|
267
|
+
def self.calc_income_tax_assessment(
|
|
268
|
+
total_income_amount,
|
|
269
|
+
date,
|
|
270
|
+
apply_reconstruction_tax: true,
|
|
271
|
+
spouse: nil,
|
|
272
|
+
dependent: {},
|
|
273
|
+
social_insurance_premium_paid: 0,
|
|
274
|
+
medical: nil,
|
|
275
|
+
life_insurance: nil,
|
|
276
|
+
donation: nil
|
|
277
|
+
)
|
|
278
|
+
unless date.is_a?(::Date) || date.is_a?(::DateTime)
|
|
279
|
+
raise TypeError,
|
|
280
|
+
"date には Date または DateTime を指定してください (got #{date.class})"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
r = Internal::CFFI.calc_income_tax_assessment(
|
|
284
|
+
{
|
|
285
|
+
total_income_amount: total_income_amount,
|
|
286
|
+
year: date.year,
|
|
287
|
+
month: date.month,
|
|
288
|
+
day: date.day,
|
|
289
|
+
spouse: spouse,
|
|
290
|
+
dependent: dependent,
|
|
291
|
+
social_insurance_premium_paid: social_insurance_premium_paid,
|
|
292
|
+
medical: medical,
|
|
293
|
+
life_insurance: life_insurance,
|
|
294
|
+
donation: donation,
|
|
295
|
+
},
|
|
296
|
+
apply_reconstruction_tax
|
|
297
|
+
)
|
|
298
|
+
IncomeTaxAssessmentResult.new(r)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# ── 印紙税 ──────────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
module StampTax
|
|
305
|
+
DOCUMENT_CODE_MAP = {
|
|
306
|
+
article1_real_estate_transfer: 1,
|
|
307
|
+
article1_other_transfer: 2,
|
|
308
|
+
article1_land_lease_or_surface_right: 3,
|
|
309
|
+
article1_consumption_loan: 4,
|
|
310
|
+
article1_transportation: 5,
|
|
311
|
+
article2_construction_work: 6,
|
|
312
|
+
article2_general_contract: 7,
|
|
313
|
+
article3_bill_amount_table: 8,
|
|
314
|
+
article3_bill_special_flat_200: 9,
|
|
315
|
+
article4_security_certificate: 10,
|
|
316
|
+
article5_merger_or_split: 11,
|
|
317
|
+
article6_articles_of_incorporation: 12,
|
|
318
|
+
article7_continuing_transaction_basic: 13,
|
|
319
|
+
article8_deposit_certificate: 14,
|
|
320
|
+
article9_transport_certificate: 15,
|
|
321
|
+
article10_insurance_certificate: 16,
|
|
322
|
+
article11_letter_of_credit: 17,
|
|
323
|
+
article12_trust_contract: 18,
|
|
324
|
+
article13_debt_guarantee: 19,
|
|
325
|
+
article14_deposit_contract: 20,
|
|
326
|
+
article15_assignment_or_assumption: 21,
|
|
327
|
+
article16_dividend_receipt: 22,
|
|
328
|
+
article17_sales_receipt: 23,
|
|
329
|
+
article17_other_receipt: 24,
|
|
330
|
+
article18_passbook: 25,
|
|
331
|
+
article19_misc_passbook: 26,
|
|
332
|
+
article20_seal_book: 27
|
|
333
|
+
}.freeze
|
|
334
|
+
|
|
335
|
+
FLAG_BIT_MAP = {
|
|
336
|
+
article3_copy_or_transcript_exempt: 1 << 0,
|
|
337
|
+
article4_specified_issuer_exempt: 1 << 1,
|
|
338
|
+
article4_restricted_beneficiary_certificate_exempt: 1 << 2,
|
|
339
|
+
article6_notary_copy_exempt: 1 << 3,
|
|
340
|
+
article8_small_deposit_exempt: 1 << 4,
|
|
341
|
+
article13_identity_guarantee_exempt: 1 << 5,
|
|
342
|
+
article17_non_business_exempt: 1 << 6,
|
|
343
|
+
article17_appended_receipt_exempt: 1 << 7,
|
|
344
|
+
article18_specified_financial_institution_exempt: 1 << 8,
|
|
345
|
+
article18_income_tax_exempt_passbook: 1 << 9,
|
|
346
|
+
article18_tax_reserve_deposit_passbook: 1 << 10
|
|
347
|
+
}.freeze
|
|
348
|
+
|
|
349
|
+
# 印紙税の計算結果。
|
|
350
|
+
class StampTaxResult
|
|
351
|
+
attr_reader :tax_amount, :rule_label, :applied_special_rule
|
|
352
|
+
|
|
353
|
+
def initialize(r)
|
|
354
|
+
@tax_amount = r.tax_amount
|
|
355
|
+
@rule_label = r.rule_label
|
|
356
|
+
@applied_special_rule = r.applied_special_rule
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def inspect
|
|
360
|
+
"#<JLawRuby::StampTax::StampTaxResult " \
|
|
361
|
+
"tax_amount=#{@tax_amount} " \
|
|
362
|
+
"rule_label=#{@rule_label.inspect} " \
|
|
363
|
+
"applied_special_rule=#{@applied_special_rule.inspect}>"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
alias to_s inspect
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# 印紙税法 別表第一に基づく印紙税額を計算する。
|
|
370
|
+
#
|
|
371
|
+
# @param document_code [Symbol, String] 文書コード
|
|
372
|
+
# @param stated_amount [Integer, nil] 記載金額。記載がない場合は nil
|
|
373
|
+
# @param date [Date] 契約書作成日
|
|
374
|
+
# @param flags [Array<Symbol,String>] 主な非課税文書フラグ
|
|
375
|
+
# @return [StampTaxResult]
|
|
376
|
+
# @raise [TypeError] date / document_code / flags の型が不正な場合
|
|
377
|
+
# @raise [RuntimeError] 計算エラーが発生した場合
|
|
378
|
+
def self.calc_stamp_tax(document_code, stated_amount, date, flags: [])
|
|
379
|
+
unless date.is_a?(::Date) || date.is_a?(::DateTime)
|
|
380
|
+
raise TypeError,
|
|
381
|
+
"date には Date または DateTime を指定してください (got #{date.class})"
|
|
382
|
+
end
|
|
383
|
+
document_code_value = normalize_document_code(document_code)
|
|
384
|
+
flags_bitset = normalize_flags(flags)
|
|
385
|
+
|
|
386
|
+
r = Internal::CFFI.calc_stamp_tax(
|
|
387
|
+
document_code_value,
|
|
388
|
+
stated_amount,
|
|
389
|
+
date.year,
|
|
390
|
+
date.month,
|
|
391
|
+
date.day,
|
|
392
|
+
flags_bitset
|
|
393
|
+
)
|
|
394
|
+
StampTaxResult.new(r)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def self.normalize_document_code(document_code)
|
|
398
|
+
key = case document_code
|
|
399
|
+
when Symbol
|
|
400
|
+
document_code
|
|
401
|
+
when String
|
|
402
|
+
document_code.to_sym
|
|
403
|
+
else
|
|
404
|
+
raise TypeError,
|
|
405
|
+
"document_code には Symbol または String を指定してください " \
|
|
406
|
+
"(got #{document_code.class})"
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
DOCUMENT_CODE_MAP.fetch(key)
|
|
410
|
+
rescue KeyError
|
|
411
|
+
raise ArgumentError,
|
|
412
|
+
"unsupported stamp tax document_code: #{document_code}"
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def self.normalize_flags(flags)
|
|
416
|
+
unless flags.is_a?(Array)
|
|
417
|
+
raise TypeError, "flags には Array を指定してください (got #{flags.class})"
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
flags.reduce(0) do |mask, flag|
|
|
421
|
+
key = case flag
|
|
422
|
+
when Symbol
|
|
423
|
+
flag
|
|
424
|
+
when String
|
|
425
|
+
flag.to_sym
|
|
426
|
+
else
|
|
427
|
+
raise TypeError,
|
|
428
|
+
"flags の各要素には Symbol または String を指定してください " \
|
|
429
|
+
"(got #{flag.class})"
|
|
430
|
+
end
|
|
431
|
+
mask | FLAG_BIT_MAP.fetch(key)
|
|
432
|
+
rescue KeyError
|
|
433
|
+
raise ArgumentError, "unsupported stamp tax flag: #{flag}"
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
private_class_method :normalize_document_code, :normalize_flags
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# ── 源泉徴収 ────────────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
module WithholdingTax
|
|
443
|
+
MANUSCRIPT_AND_LECTURE = 1
|
|
444
|
+
PROFESSIONAL_FEE = 2
|
|
445
|
+
EXCLUSIVE_CONTRACT_FEE = 3
|
|
446
|
+
|
|
447
|
+
# 源泉徴収税額の計算結果。
|
|
448
|
+
class WithholdingTaxResult
|
|
449
|
+
attr_reader :gross_payment_amount, :taxable_payment_amount, :tax_amount,
|
|
450
|
+
:net_payment_amount, :category, :breakdown
|
|
451
|
+
|
|
452
|
+
def initialize(r)
|
|
453
|
+
@gross_payment_amount = r.gross_payment_amount
|
|
454
|
+
@taxable_payment_amount = r.taxable_payment_amount
|
|
455
|
+
@tax_amount = r.tax_amount
|
|
456
|
+
@net_payment_amount = r.net_payment_amount
|
|
457
|
+
@category = self.class.category_to_symbol(r.category)
|
|
458
|
+
@submission_prize_exempted = r.submission_prize_exempted
|
|
459
|
+
@breakdown = r.breakdown.map do |step|
|
|
460
|
+
{
|
|
461
|
+
label: step.label,
|
|
462
|
+
base_amount: step.base_amount,
|
|
463
|
+
rate_numer: step.rate_numer,
|
|
464
|
+
rate_denom: step.rate_denom,
|
|
465
|
+
result: step.result,
|
|
466
|
+
}
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def self.category_to_symbol(category)
|
|
471
|
+
case category
|
|
472
|
+
when MANUSCRIPT_AND_LECTURE then :manuscript_and_lecture
|
|
473
|
+
when PROFESSIONAL_FEE then :professional_fee
|
|
474
|
+
when EXCLUSIVE_CONTRACT_FEE then :exclusive_contract_fee
|
|
475
|
+
else category
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def submission_prize_exempted?
|
|
480
|
+
@submission_prize_exempted
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def inspect
|
|
484
|
+
"#<JLawRuby::WithholdingTax::WithholdingTaxResult " \
|
|
485
|
+
"gross_payment_amount=#{@gross_payment_amount} " \
|
|
486
|
+
"taxable_payment_amount=#{@taxable_payment_amount} " \
|
|
487
|
+
"tax_amount=#{@tax_amount} " \
|
|
488
|
+
"net_payment_amount=#{@net_payment_amount} " \
|
|
489
|
+
"category=#{@category.inspect} " \
|
|
490
|
+
"submission_prize_exempted=#{@submission_prize_exempted}>"
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
alias to_s inspect
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def self.calc_withholding_tax(
|
|
497
|
+
payment_amount,
|
|
498
|
+
date,
|
|
499
|
+
category,
|
|
500
|
+
is_submission_prize: false,
|
|
501
|
+
separated_consumption_tax_amount: 0
|
|
502
|
+
)
|
|
503
|
+
unless date.is_a?(::Date) || date.is_a?(::DateTime)
|
|
504
|
+
raise TypeError,
|
|
505
|
+
"date には Date または DateTime を指定してください (got #{date.class})"
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
r = Internal::CFFI.calc_withholding_tax(
|
|
509
|
+
payment_amount,
|
|
510
|
+
separated_consumption_tax_amount,
|
|
511
|
+
date.year,
|
|
512
|
+
date.month,
|
|
513
|
+
date.day,
|
|
514
|
+
normalize_category(category),
|
|
515
|
+
is_submission_prize
|
|
516
|
+
)
|
|
517
|
+
WithholdingTaxResult.new(r)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def self.normalize_category(category)
|
|
521
|
+
case category
|
|
522
|
+
when Integer then category
|
|
523
|
+
when String then normalize_category(category.to_sym)
|
|
524
|
+
when :manuscript_and_lecture then MANUSCRIPT_AND_LECTURE
|
|
525
|
+
when :professional_fee then PROFESSIONAL_FEE
|
|
526
|
+
when :exclusive_contract_fee then EXCLUSIVE_CONTRACT_FEE
|
|
527
|
+
else
|
|
528
|
+
raise ArgumentError, "unknown withholding tax category: #{category.inspect}"
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module JLawRuby
|
|
6
|
+
module VendorRust
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
WORKSPACE_TOML = <<~TOML
|
|
10
|
+
[workspace]
|
|
11
|
+
members = [
|
|
12
|
+
"crates/j-law-core",
|
|
13
|
+
"crates/j-law-registry",
|
|
14
|
+
"crates/j-law-c-ffi",
|
|
15
|
+
]
|
|
16
|
+
resolver = "2"
|
|
17
|
+
|
|
18
|
+
[workspace.lints.clippy]
|
|
19
|
+
disallowed_methods = "warn"
|
|
20
|
+
disallowed_types = "warn"
|
|
21
|
+
disallowed_macros = "warn"
|
|
22
|
+
TOML
|
|
23
|
+
|
|
24
|
+
COPY_MAP = {
|
|
25
|
+
"crates/j-law-c-ffi" => %w[Cargo.toml src j_law_c_ffi.h],
|
|
26
|
+
"crates/j-law-core" => %w[Cargo.toml src],
|
|
27
|
+
"crates/j-law-registry" => %w[Cargo.toml src data],
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def prepare!(gem_root)
|
|
31
|
+
vendor_root = File.join(gem_root, "vendor", "rust")
|
|
32
|
+
repo_root = File.expand_path("../..", gem_root)
|
|
33
|
+
|
|
34
|
+
FileUtils.rm_rf(vendor_root)
|
|
35
|
+
FileUtils.mkdir_p(vendor_root)
|
|
36
|
+
File.write(File.join(vendor_root, "Cargo.toml"), WORKSPACE_TOML)
|
|
37
|
+
|
|
38
|
+
cargo_lock = File.join(repo_root, "Cargo.lock")
|
|
39
|
+
FileUtils.cp(cargo_lock, File.join(vendor_root, "Cargo.lock")) if File.file?(cargo_lock)
|
|
40
|
+
|
|
41
|
+
COPY_MAP.each do |crate_dir, entries|
|
|
42
|
+
entries.each do |entry|
|
|
43
|
+
source = File.join(repo_root, crate_dir, entry)
|
|
44
|
+
destination = File.join(vendor_root, crate_dir, entry)
|
|
45
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
46
|
+
FileUtils.cp_r(source, destination)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "j_law_ruby"
|
|
5
|
+
|
|
6
|
+
class TestCFFIAdapter < Minitest::Test
|
|
7
|
+
def test_ffi_version_matches
|
|
8
|
+
assert_equal 4, JLawRuby::Internal::CFFI.ffi_version
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_compiled_library_is_loaded_from_gem_path
|
|
12
|
+
expected_path = File.expand_path(
|
|
13
|
+
"../lib/j_law_ruby/native/#{JLawRuby::BuildSupport.shared_library_filename}",
|
|
14
|
+
__dir__
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
assert_equal expected_path, JLawRuby::Internal::CFFI.library_path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_fixed_length_strings_are_restored
|
|
21
|
+
brokerage = JLawRuby::Internal::CFFI.calc_brokerage_fee(5_000_000, 2024, 8, 1, false, false)
|
|
22
|
+
assert_equal %w[tier1 tier2 tier3], brokerage.breakdown.map(&:label)
|
|
23
|
+
|
|
24
|
+
stamp = JLawRuby::Internal::CFFI.calc_stamp_tax(2, 5_000_000, 2024, 8, 1, 0)
|
|
25
|
+
refute_empty stamp.rule_label
|
|
26
|
+
|
|
27
|
+
withholding = JLawRuby::Internal::CFFI.calc_withholding_tax(1_500_000, 0, 2026, 1, 1, 2, false)
|
|
28
|
+
assert_equal 2, withholding.breakdown.length
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_error_path_raises_runtime_error
|
|
32
|
+
error = assert_raises(RuntimeError) do
|
|
33
|
+
JLawRuby::Internal::CFFI.calc_consumption_tax(100_000, 2016, 1, 1, true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
refute_empty error.message
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_invalid_date_parts_are_rejected
|
|
40
|
+
error = assert_raises(ArgumentError) do
|
|
41
|
+
JLawRuby::Internal::CFFI.calc_brokerage_fee(5_000_000, 2024, 13, 1, false, false)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
assert_match(/2024-13-01/, error.message)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "json"
|
|
5
|
+
require "date"
|
|
6
|
+
require "j_law_ruby"
|
|
7
|
+
|
|
8
|
+
# 消費税法第29条に基づく消費税額計算のテスト。
|
|
9
|
+
#
|
|
10
|
+
# 法的根拠: 消費税法 第29条(税率)
|
|
11
|
+
# テストケースは tests/fixtures/consumption_tax.json から読み込む。
|
|
12
|
+
class TestConsumptionTax < Minitest::Test
|
|
13
|
+
FIXTURES = JSON.parse(File.read(File.join(__dir__, "../../../tests/fixtures/consumption_tax.json")))
|
|
14
|
+
|
|
15
|
+
# ─── データ駆動テスト ─────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
FIXTURES["consumption_tax"].each do |tc|
|
|
18
|
+
define_method("test_#{tc['id']}") do
|
|
19
|
+
inp = tc["input"]
|
|
20
|
+
exp = tc["expected"]
|
|
21
|
+
|
|
22
|
+
date = Date.parse(inp["date"])
|
|
23
|
+
result = JLawRuby::ConsumptionTax.calc_consumption_tax(
|
|
24
|
+
inp["amount"],
|
|
25
|
+
date,
|
|
26
|
+
inp["is_reduced_rate"]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
assert_equal exp["tax_amount"], result.tax_amount, "#{tc['id']}: tax_amount"
|
|
30
|
+
assert_equal exp["amount_with_tax"], result.amount_with_tax, "#{tc['id']}: amount_with_tax"
|
|
31
|
+
assert_equal exp["amount_without_tax"], result.amount_without_tax, "#{tc['id']}: amount_without_tax"
|
|
32
|
+
assert_equal exp["applied_rate_numer"], result.applied_rate_numer, "#{tc['id']}: applied_rate_numer"
|
|
33
|
+
assert_equal exp["applied_rate_denom"], result.applied_rate_denom, "#{tc['id']}: applied_rate_denom"
|
|
34
|
+
assert_equal exp["is_reduced_rate"], result.is_reduced_rate?, "#{tc['id']}: is_reduced_rate"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# ─── 言語固有テスト ───────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
def test_error_reduced_rate_without_support
|
|
41
|
+
err = assert_raises(RuntimeError) do
|
|
42
|
+
JLawRuby::ConsumptionTax.calc_consumption_tax(100_000, Date.new(2016, 1, 1), true)
|
|
43
|
+
end
|
|
44
|
+
refute_nil err.message
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_before_introduction_no_tax
|
|
48
|
+
result = JLawRuby::ConsumptionTax.calc_consumption_tax(100_000, Date.new(1988, 1, 1), false)
|
|
49
|
+
assert_equal 0, result.tax_amount
|
|
50
|
+
assert_equal 100_000, result.amount_with_tax
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_inspect
|
|
54
|
+
result = JLawRuby::ConsumptionTax.calc_consumption_tax(100_000, Date.new(2024, 1, 1), false)
|
|
55
|
+
assert_match(/ConsumptionTaxResult/, result.inspect)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_type_error_invalid_date
|
|
59
|
+
assert_raises(TypeError) do
|
|
60
|
+
JLawRuby::ConsumptionTax.calc_consumption_tax(100_000, "2024-01-01", false)
|
|
61
|
+
end
|
|
62
|
+
assert_raises(TypeError) do
|
|
63
|
+
JLawRuby::ConsumptionTax.calc_consumption_tax(100_000, 20_240_101, false)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|