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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +9 -0
  3. data/README.md +109 -0
  4. data/Rakefile +87 -0
  5. data/ext/j_law_ruby/extconf.rb +34 -0
  6. data/lib/j_law_ruby/build_support.rb +129 -0
  7. data/lib/j_law_ruby/c_ffi.rb +662 -0
  8. data/lib/j_law_ruby.rb +532 -0
  9. data/rake_support/vendor_rust.rb +51 -0
  10. data/test/test_c_ffi_adapter.rb +46 -0
  11. data/test/test_consumption_tax.rb +66 -0
  12. data/test/test_gemspec.rb +82 -0
  13. data/test/test_income_tax.rb +77 -0
  14. data/test/test_income_tax_deductions.rb +82 -0
  15. data/test/test_real_estate.rb +98 -0
  16. data/test/test_stamp_tax.rb +68 -0
  17. data/test/test_withholding_tax.rb +65 -0
  18. data/vendor/rust/Cargo.lock +235 -0
  19. data/vendor/rust/Cargo.toml +12 -0
  20. data/vendor/rust/crates/j-law-c-ffi/Cargo.toml +20 -0
  21. data/vendor/rust/crates/j-law-c-ffi/j_law_c_ffi.h +493 -0
  22. data/vendor/rust/crates/j-law-c-ffi/src/lib.rs +1553 -0
  23. data/vendor/rust/crates/j-law-core/Cargo.toml +18 -0
  24. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/calculator.rs +216 -0
  25. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/context.rs +29 -0
  26. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/mod.rs +9 -0
  27. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/params.rs +24 -0
  28. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/policy.rs +34 -0
  29. data/vendor/rust/crates/j-law-core/src/domains/income_tax/assessment.rs +76 -0
  30. data/vendor/rust/crates/j-law-core/src/domains/income_tax/calculator.rs +222 -0
  31. data/vendor/rust/crates/j-law-core/src/domains/income_tax/context.rs +79 -0
  32. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/calculator.rs +167 -0
  33. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/context.rs +172 -0
  34. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/expense.rs +465 -0
  35. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/mod.rs +20 -0
  36. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/params.rs +205 -0
  37. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/personal.rs +324 -0
  38. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/types.rs +61 -0
  39. data/vendor/rust/crates/j-law-core/src/domains/income_tax/mod.rs +24 -0
  40. data/vendor/rust/crates/j-law-core/src/domains/income_tax/params.rs +109 -0
  41. data/vendor/rust/crates/j-law-core/src/domains/income_tax/policy.rs +103 -0
  42. data/vendor/rust/crates/j-law-core/src/domains/mod.rs +5 -0
  43. data/vendor/rust/crates/j-law-core/src/domains/real_estate/calculator.rs +197 -0
  44. data/vendor/rust/crates/j-law-core/src/domains/real_estate/context.rs +48 -0
  45. data/vendor/rust/crates/j-law-core/src/domains/real_estate/mod.rs +9 -0
  46. data/vendor/rust/crates/j-law-core/src/domains/real_estate/params.rs +43 -0
  47. data/vendor/rust/crates/j-law-core/src/domains/real_estate/policy.rs +40 -0
  48. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/calculator.rs +321 -0
  49. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/context.rs +408 -0
  50. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/mod.rs +12 -0
  51. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/params.rs +190 -0
  52. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/policy.rs +105 -0
  53. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/calculator.rs +247 -0
  54. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/context.rs +167 -0
  55. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/mod.rs +9 -0
  56. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/params.rs +80 -0
  57. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/policy.rs +49 -0
  58. data/vendor/rust/crates/j-law-core/src/error.rs +171 -0
  59. data/vendor/rust/crates/j-law-core/src/lib.rs +9 -0
  60. data/vendor/rust/crates/j-law-core/src/types/amount.rs +232 -0
  61. data/vendor/rust/crates/j-law-core/src/types/citation.rs +82 -0
  62. data/vendor/rust/crates/j-law-core/src/types/date.rs +280 -0
  63. data/vendor/rust/crates/j-law-core/src/types/mod.rs +11 -0
  64. data/vendor/rust/crates/j-law-core/src/types/rate.rs +219 -0
  65. data/vendor/rust/crates/j-law-core/src/types/rounding.rs +81 -0
  66. data/vendor/rust/crates/j-law-registry/Cargo.toml +15 -0
  67. data/vendor/rust/crates/j-law-registry/data/consumption_tax/consumption_tax.json +70 -0
  68. data/vendor/rust/crates/j-law-registry/data/income_tax/deductions.json +327 -0
  69. data/vendor/rust/crates/j-law-registry/data/income_tax/income_tax.json +352 -0
  70. data/vendor/rust/crates/j-law-registry/data/real_estate/brokerage_fee.json +125 -0
  71. data/vendor/rust/crates/j-law-registry/data/stamp_tax/stamp_tax.json +674 -0
  72. data/vendor/rust/crates/j-law-registry/data/withholding_tax/withholding_tax.json +70 -0
  73. data/vendor/rust/crates/j-law-registry/src/consumption_tax_loader.rs +325 -0
  74. data/vendor/rust/crates/j-law-registry/src/consumption_tax_schema.rs +49 -0
  75. data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_loader.rs +636 -0
  76. data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_schema.rs +111 -0
  77. data/vendor/rust/crates/j-law-registry/src/income_tax_loader.rs +445 -0
  78. data/vendor/rust/crates/j-law-registry/src/income_tax_schema.rs +44 -0
  79. data/vendor/rust/crates/j-law-registry/src/lib.rs +20 -0
  80. data/vendor/rust/crates/j-law-registry/src/loader.rs +221 -0
  81. data/vendor/rust/crates/j-law-registry/src/schema.rs +73 -0
  82. data/vendor/rust/crates/j-law-registry/src/stamp_tax_loader.rs +374 -0
  83. data/vendor/rust/crates/j-law-registry/src/stamp_tax_schema.rs +72 -0
  84. data/vendor/rust/crates/j-law-registry/src/validator.rs +204 -0
  85. data/vendor/rust/crates/j-law-registry/src/withholding_tax_loader.rs +310 -0
  86. data/vendor/rust/crates/j-law-registry/src/withholding_tax_schema.rs +61 -0
  87. 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