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
@@ -0,0 +1,662 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "ffi"
5
+ require_relative "build_support"
6
+
7
+ module JLawRuby
8
+ module Internal
9
+ module CFFI
10
+ extend FFI::Library
11
+
12
+ FFI_VERSION = 4
13
+ MAX_TIERS = 8
14
+ MAX_DEDUCTION_LINES = 8
15
+ LABEL_LEN = 64
16
+ ERROR_BUF_LEN = 256
17
+ UINT64_MAX = (1 << 64) - 1
18
+ UINT32_MAX = (1 << 32) - 1
19
+ UINT16_MAX = (1 << 16) - 1
20
+ UINT8_MAX = (1 << 8) - 1
21
+ GEM_ROOT = File.expand_path("../..", __dir__)
22
+ LIBRARY_PATH = BuildSupport.resolve_shared_library_path(GEM_ROOT)
23
+
24
+ BreakdownStepRecord = Struct.new(
25
+ :label, :base_amount, :rate_numer, :rate_denom, :result,
26
+ keyword_init: true
27
+ )
28
+ BrokerageFeeRecord = Struct.new(
29
+ :total_without_tax, :total_with_tax, :tax_amount, :low_cost_special_applied, :breakdown,
30
+ keyword_init: true
31
+ )
32
+ IncomeTaxStepRecord = Struct.new(
33
+ :label, :taxable_income, :rate_numer, :rate_denom, :deduction, :result,
34
+ keyword_init: true
35
+ )
36
+ IncomeTaxRecord = Struct.new(
37
+ :base_tax, :reconstruction_tax, :total_tax, :reconstruction_tax_applied, :breakdown,
38
+ keyword_init: true
39
+ )
40
+ IncomeDeductionLineRecord = Struct.new(
41
+ :kind, :label, :amount,
42
+ keyword_init: true
43
+ )
44
+ IncomeDeductionRecord = Struct.new(
45
+ :total_income_amount, :total_deductions,
46
+ :taxable_income_before_truncation, :taxable_income, :breakdown,
47
+ keyword_init: true
48
+ )
49
+ IncomeTaxAssessmentRecord = Struct.new(
50
+ :deductions, :tax,
51
+ keyword_init: true
52
+ )
53
+ ConsumptionTaxRecord = Struct.new(
54
+ :tax_amount, :amount_with_tax, :amount_without_tax,
55
+ :applied_rate_numer, :applied_rate_denom, :is_reduced_rate,
56
+ keyword_init: true
57
+ )
58
+ StampTaxRecord = Struct.new(
59
+ :tax_amount, :rule_label, :applied_special_rule,
60
+ keyword_init: true
61
+ )
62
+ WithholdingTaxRecord = Struct.new(
63
+ :gross_payment_amount, :taxable_payment_amount, :tax_amount, :net_payment_amount,
64
+ :category, :submission_prize_exempted, :breakdown,
65
+ keyword_init: true
66
+ )
67
+
68
+ unless LIBRARY_PATH
69
+ raise LoadError,
70
+ "j-law-c-ffi shared library was not found. Run `bundle exec rake compile` first."
71
+ end
72
+
73
+ ffi_lib LIBRARY_PATH
74
+
75
+ class BreakdownStepStruct < FFI::Struct
76
+ layout :label, [:char, LABEL_LEN],
77
+ :base_amount, :uint64,
78
+ :rate_numer, :uint64,
79
+ :rate_denom, :uint64,
80
+ :result, :uint64
81
+ end
82
+
83
+ class BrokerageFeeStruct < FFI::Struct
84
+ layout :total_without_tax, :uint64,
85
+ :total_with_tax, :uint64,
86
+ :tax_amount, :uint64,
87
+ :low_cost_special_applied, :int,
88
+ :breakdown_padding, [:char, 4],
89
+ :breakdown_storage, [:char, BreakdownStepStruct.size * MAX_TIERS],
90
+ :breakdown_len, :int
91
+ end
92
+
93
+ class IncomeTaxStepStruct < FFI::Struct
94
+ layout :label, [:char, LABEL_LEN],
95
+ :taxable_income, :uint64,
96
+ :rate_numer, :uint64,
97
+ :rate_denom, :uint64,
98
+ :deduction, :uint64,
99
+ :result, :uint64
100
+ end
101
+
102
+ class IncomeTaxStruct < FFI::Struct
103
+ layout :base_tax, :uint64,
104
+ :reconstruction_tax, :uint64,
105
+ :total_tax, :uint64,
106
+ :reconstruction_tax_applied, :int,
107
+ :breakdown_padding, [:char, 4],
108
+ :breakdown_storage, [:char, IncomeTaxStepStruct.size * MAX_TIERS],
109
+ :breakdown_len, :int
110
+ end
111
+
112
+ class IncomeDeductionLineStruct < FFI::Struct
113
+ layout :kind, :uint32,
114
+ :label, [:char, LABEL_LEN],
115
+ :amount, :uint64
116
+ end
117
+
118
+ class IncomeDeductionInputStruct < FFI::Struct
119
+ layout :total_income_amount, :uint64,
120
+ :year, :uint16,
121
+ :month, :uint8,
122
+ :day, :uint8,
123
+ :has_spouse, :int,
124
+ :spouse_total_income_amount, :uint64,
125
+ :spouse_is_same_household, :int,
126
+ :spouse_is_elderly, :int,
127
+ :dependent_general_count, :uint64,
128
+ :dependent_specific_count, :uint64,
129
+ :dependent_elderly_cohabiting_count, :uint64,
130
+ :dependent_elderly_other_count, :uint64,
131
+ :social_insurance_premium_paid, :uint64,
132
+ :has_medical, :int,
133
+ :medical_expense_paid, :uint64,
134
+ :medical_reimbursed_amount, :uint64,
135
+ :has_life_insurance, :int,
136
+ :life_new_general_paid_amount, :uint64,
137
+ :life_new_individual_pension_paid_amount, :uint64,
138
+ :life_new_care_medical_paid_amount, :uint64,
139
+ :life_old_general_paid_amount, :uint64,
140
+ :life_old_individual_pension_paid_amount, :uint64,
141
+ :has_donation, :int,
142
+ :donation_qualified_amount, :uint64
143
+ end
144
+
145
+ class IncomeDeductionStruct < FFI::Struct
146
+ layout :total_income_amount, :uint64,
147
+ :total_deductions, :uint64,
148
+ :taxable_income_before_truncation, :uint64,
149
+ :taxable_income, :uint64,
150
+ :breakdown_storage, [:char, IncomeDeductionLineStruct.size * MAX_DEDUCTION_LINES],
151
+ :breakdown_len, :int
152
+ end
153
+
154
+ class IncomeTaxAssessmentStruct < FFI::Struct
155
+ layout :total_income_amount, :uint64,
156
+ :total_deductions, :uint64,
157
+ :taxable_income_before_truncation, :uint64,
158
+ :taxable_income, :uint64,
159
+ :base_tax, :uint64,
160
+ :reconstruction_tax, :uint64,
161
+ :total_tax, :uint64,
162
+ :reconstruction_tax_applied, :int,
163
+ :deduction_padding, [:char, 4],
164
+ :deduction_breakdown_storage, [:char, IncomeDeductionLineStruct.size * MAX_DEDUCTION_LINES],
165
+ :deduction_breakdown_len, :int,
166
+ :tax_padding, [:char, 4],
167
+ :tax_breakdown_storage, [:char, IncomeTaxStepStruct.size * MAX_TIERS],
168
+ :tax_breakdown_len, :int
169
+ end
170
+
171
+ class ConsumptionTaxStruct < FFI::Struct
172
+ layout :tax_amount, :uint64,
173
+ :amount_with_tax, :uint64,
174
+ :amount_without_tax, :uint64,
175
+ :applied_rate_numer, :uint64,
176
+ :applied_rate_denom, :uint64,
177
+ :is_reduced_rate, :int
178
+ end
179
+
180
+ class StampTaxStruct < FFI::Struct
181
+ layout :tax_amount, :uint64,
182
+ :rule_label, [:char, LABEL_LEN],
183
+ :applied_special_rule, [:char, LABEL_LEN]
184
+ end
185
+
186
+ class WithholdingTaxStruct < FFI::Struct
187
+ layout :gross_payment_amount, :uint64,
188
+ :taxable_payment_amount, :uint64,
189
+ :tax_amount, :uint64,
190
+ :net_payment_amount, :uint64,
191
+ :category, :uint32,
192
+ :submission_prize_exempted, :int,
193
+ :breakdown_storage, [:char, BreakdownStepStruct.size * MAX_TIERS],
194
+ :breakdown_len, :int
195
+ end
196
+
197
+ attach_function :j_law_c_ffi_version, [], :uint32
198
+ attach_function :j_law_calc_brokerage_fee,
199
+ [:uint64, :uint16, :uint8, :uint8, :int, :int,
200
+ BrokerageFeeStruct.by_ref, :pointer, :int],
201
+ :int
202
+ attach_function :j_law_calc_income_tax,
203
+ [:uint64, :uint16, :uint8, :uint8, :int,
204
+ IncomeTaxStruct.by_ref, :pointer, :int],
205
+ :int
206
+ attach_function :j_law_calc_income_deductions,
207
+ [IncomeDeductionInputStruct.by_ref,
208
+ IncomeDeductionStruct.by_ref, :pointer, :int],
209
+ :int
210
+ attach_function :j_law_calc_income_tax_assessment,
211
+ [IncomeDeductionInputStruct.by_ref, :int,
212
+ IncomeTaxAssessmentStruct.by_ref, :pointer, :int],
213
+ :int
214
+ attach_function :j_law_calc_consumption_tax,
215
+ [:uint64, :uint16, :uint8, :uint8, :int,
216
+ ConsumptionTaxStruct.by_ref, :pointer, :int],
217
+ :int
218
+ attach_function :j_law_calc_stamp_tax,
219
+ [:uint32, :uint64, :int, :uint16, :uint8, :uint8, :uint64,
220
+ StampTaxStruct.by_ref, :pointer, :int],
221
+ :int
222
+ attach_function :j_law_calc_withholding_tax,
223
+ [:uint64, :uint64, :uint16, :uint8, :uint8, :uint32, :int,
224
+ WithholdingTaxStruct.by_ref, :pointer, :int],
225
+ :int
226
+
227
+ actual_ffi_version = j_law_c_ffi_version
228
+ if actual_ffi_version != FFI_VERSION
229
+ raise LoadError,
230
+ "j-law-c-ffi FFI version mismatch: expected #{FFI_VERSION}, got #{actual_ffi_version}"
231
+ end
232
+
233
+ module_function
234
+
235
+ def library_path
236
+ LIBRARY_PATH
237
+ end
238
+
239
+ def ffi_version
240
+ j_law_c_ffi_version
241
+ end
242
+
243
+ def calc_brokerage_fee(price, year, month, day, is_low_cost_vacant_house, is_seller)
244
+ validated_price = validate_u64(price, "price")
245
+ validated_year, validated_month, validated_day = validate_date_parts(year, month, day)
246
+ result = BrokerageFeeStruct.new
247
+
248
+ call_with_error do |error_buf|
249
+ j_law_calc_brokerage_fee(
250
+ validated_price,
251
+ validated_year,
252
+ validated_month,
253
+ validated_day,
254
+ bool_to_c_int(is_low_cost_vacant_house),
255
+ bool_to_c_int(is_seller),
256
+ result,
257
+ error_buf,
258
+ ERROR_BUF_LEN
259
+ )
260
+ end
261
+
262
+ BrokerageFeeRecord.new(
263
+ total_without_tax: result[:total_without_tax],
264
+ total_with_tax: result[:total_with_tax],
265
+ tax_amount: result[:tax_amount],
266
+ low_cost_special_applied: c_int_to_bool(result[:low_cost_special_applied]),
267
+ breakdown: read_struct_array(
268
+ result.pointer + BrokerageFeeStruct.offset_of(:breakdown_storage),
269
+ BreakdownStepStruct,
270
+ result[:breakdown_len]
271
+ ).map do |step|
272
+ BreakdownStepRecord.new(
273
+ label: read_fixed_string(step, :label, LABEL_LEN),
274
+ base_amount: step[:base_amount],
275
+ rate_numer: step[:rate_numer],
276
+ rate_denom: step[:rate_denom],
277
+ result: step[:result]
278
+ )
279
+ end
280
+ )
281
+ end
282
+
283
+ def calc_income_tax(taxable_income, year, month, day, apply_reconstruction_tax)
284
+ validated_taxable_income = validate_u64(taxable_income, "taxable_income")
285
+ validated_year, validated_month, validated_day = validate_date_parts(year, month, day)
286
+ result = IncomeTaxStruct.new
287
+
288
+ call_with_error do |error_buf|
289
+ j_law_calc_income_tax(
290
+ validated_taxable_income,
291
+ validated_year,
292
+ validated_month,
293
+ validated_day,
294
+ bool_to_c_int(apply_reconstruction_tax),
295
+ result,
296
+ error_buf,
297
+ ERROR_BUF_LEN
298
+ )
299
+ end
300
+
301
+ IncomeTaxRecord.new(
302
+ base_tax: result[:base_tax],
303
+ reconstruction_tax: result[:reconstruction_tax],
304
+ total_tax: result[:total_tax],
305
+ reconstruction_tax_applied: c_int_to_bool(result[:reconstruction_tax_applied]),
306
+ breakdown: read_struct_array(
307
+ result.pointer + IncomeTaxStruct.offset_of(:breakdown_storage),
308
+ IncomeTaxStepStruct,
309
+ result[:breakdown_len]
310
+ ).map do |step|
311
+ IncomeTaxStepRecord.new(
312
+ label: read_fixed_string(step, :label, LABEL_LEN),
313
+ taxable_income: step[:taxable_income],
314
+ rate_numer: step[:rate_numer],
315
+ rate_denom: step[:rate_denom],
316
+ deduction: step[:deduction],
317
+ result: step[:result]
318
+ )
319
+ end
320
+ )
321
+ end
322
+
323
+ def calc_income_deductions(input)
324
+ input_struct = build_income_deduction_input_struct(input)
325
+ result = IncomeDeductionStruct.new
326
+
327
+ call_with_error do |error_buf|
328
+ j_law_calc_income_deductions(
329
+ input_struct,
330
+ result,
331
+ error_buf,
332
+ ERROR_BUF_LEN
333
+ )
334
+ end
335
+
336
+ IncomeDeductionRecord.new(
337
+ total_income_amount: result[:total_income_amount],
338
+ total_deductions: result[:total_deductions],
339
+ taxable_income_before_truncation: result[:taxable_income_before_truncation],
340
+ taxable_income: result[:taxable_income],
341
+ breakdown: read_struct_array(
342
+ result.pointer + IncomeDeductionStruct.offset_of(:breakdown_storage),
343
+ IncomeDeductionLineStruct,
344
+ result[:breakdown_len],
345
+ max_length: MAX_DEDUCTION_LINES
346
+ ).map do |line|
347
+ IncomeDeductionLineRecord.new(
348
+ kind: line[:kind],
349
+ label: read_fixed_string(line, :label, LABEL_LEN),
350
+ amount: line[:amount]
351
+ )
352
+ end
353
+ )
354
+ end
355
+
356
+ def calc_income_tax_assessment(input, apply_reconstruction_tax)
357
+ input_struct = build_income_deduction_input_struct(input)
358
+ result = IncomeTaxAssessmentStruct.new
359
+
360
+ call_with_error do |error_buf|
361
+ j_law_calc_income_tax_assessment(
362
+ input_struct,
363
+ bool_to_c_int(apply_reconstruction_tax),
364
+ result,
365
+ error_buf,
366
+ ERROR_BUF_LEN
367
+ )
368
+ end
369
+
370
+ deductions = IncomeDeductionRecord.new(
371
+ total_income_amount: result[:total_income_amount],
372
+ total_deductions: result[:total_deductions],
373
+ taxable_income_before_truncation: result[:taxable_income_before_truncation],
374
+ taxable_income: result[:taxable_income],
375
+ breakdown: read_struct_array(
376
+ result.pointer + IncomeTaxAssessmentStruct.offset_of(:deduction_breakdown_storage),
377
+ IncomeDeductionLineStruct,
378
+ result[:deduction_breakdown_len],
379
+ max_length: MAX_DEDUCTION_LINES
380
+ ).map do |line|
381
+ IncomeDeductionLineRecord.new(
382
+ kind: line[:kind],
383
+ label: read_fixed_string(line, :label, LABEL_LEN),
384
+ amount: line[:amount]
385
+ )
386
+ end
387
+ )
388
+ tax = IncomeTaxRecord.new(
389
+ base_tax: result[:base_tax],
390
+ reconstruction_tax: result[:reconstruction_tax],
391
+ total_tax: result[:total_tax],
392
+ reconstruction_tax_applied: c_int_to_bool(result[:reconstruction_tax_applied]),
393
+ breakdown: read_struct_array(
394
+ result.pointer + IncomeTaxAssessmentStruct.offset_of(:tax_breakdown_storage),
395
+ IncomeTaxStepStruct,
396
+ result[:tax_breakdown_len]
397
+ ).map do |step|
398
+ IncomeTaxStepRecord.new(
399
+ label: read_fixed_string(step, :label, LABEL_LEN),
400
+ taxable_income: step[:taxable_income],
401
+ rate_numer: step[:rate_numer],
402
+ rate_denom: step[:rate_denom],
403
+ deduction: step[:deduction],
404
+ result: step[:result]
405
+ )
406
+ end
407
+ )
408
+
409
+ IncomeTaxAssessmentRecord.new(
410
+ deductions: deductions,
411
+ tax: tax
412
+ )
413
+ end
414
+
415
+ def calc_consumption_tax(amount, year, month, day, is_reduced_rate)
416
+ validated_amount = validate_u64(amount, "amount")
417
+ validated_year, validated_month, validated_day = validate_date_parts(year, month, day)
418
+ result = ConsumptionTaxStruct.new
419
+
420
+ call_with_error do |error_buf|
421
+ j_law_calc_consumption_tax(
422
+ validated_amount,
423
+ validated_year,
424
+ validated_month,
425
+ validated_day,
426
+ bool_to_c_int(is_reduced_rate),
427
+ result,
428
+ error_buf,
429
+ ERROR_BUF_LEN
430
+ )
431
+ end
432
+
433
+ ConsumptionTaxRecord.new(
434
+ tax_amount: result[:tax_amount],
435
+ amount_with_tax: result[:amount_with_tax],
436
+ amount_without_tax: result[:amount_without_tax],
437
+ applied_rate_numer: result[:applied_rate_numer],
438
+ applied_rate_denom: result[:applied_rate_denom],
439
+ is_reduced_rate: c_int_to_bool(result[:is_reduced_rate])
440
+ )
441
+ end
442
+
443
+ def calc_stamp_tax(document_code, stated_amount, year, month, day, flags_bitset = 0)
444
+ validated_document_code = validate_u32(document_code, "document_code")
445
+ validated_stated_amount = stated_amount.nil? ? nil : validate_u64(stated_amount, "stated_amount")
446
+ validated_year, validated_month, validated_day = validate_date_parts(year, month, day)
447
+ validated_flags_bitset = validate_u64(flags_bitset, "flags_bitset")
448
+ result = StampTaxStruct.new
449
+
450
+ call_with_error do |error_buf|
451
+ j_law_calc_stamp_tax(
452
+ validated_document_code,
453
+ validated_stated_amount || 0,
454
+ validated_stated_amount.nil? ? 0 : 1,
455
+ validated_year,
456
+ validated_month,
457
+ validated_day,
458
+ validated_flags_bitset,
459
+ result,
460
+ error_buf,
461
+ ERROR_BUF_LEN
462
+ )
463
+ end
464
+
465
+ StampTaxRecord.new(
466
+ tax_amount: result[:tax_amount],
467
+ rule_label: read_fixed_string(result, :rule_label, LABEL_LEN),
468
+ applied_special_rule: begin
469
+ value = read_fixed_string(result, :applied_special_rule, LABEL_LEN)
470
+ value.empty? ? nil : value
471
+ end
472
+ )
473
+ end
474
+
475
+ def calc_withholding_tax(
476
+ payment_amount,
477
+ separated_consumption_tax_amount,
478
+ year,
479
+ month,
480
+ day,
481
+ category,
482
+ is_submission_prize
483
+ )
484
+ validated_payment_amount = validate_u64(payment_amount, "payment_amount")
485
+ validated_separated_consumption_tax_amount =
486
+ validate_u64(separated_consumption_tax_amount, "separated_consumption_tax_amount")
487
+ validated_year, validated_month, validated_day = validate_date_parts(year, month, day)
488
+ validated_category = validate_u32(category, "category")
489
+ result = WithholdingTaxStruct.new
490
+
491
+ call_with_error do |error_buf|
492
+ j_law_calc_withholding_tax(
493
+ validated_payment_amount,
494
+ validated_separated_consumption_tax_amount,
495
+ validated_year,
496
+ validated_month,
497
+ validated_day,
498
+ validated_category,
499
+ bool_to_c_int(is_submission_prize),
500
+ result,
501
+ error_buf,
502
+ ERROR_BUF_LEN
503
+ )
504
+ end
505
+
506
+ WithholdingTaxRecord.new(
507
+ gross_payment_amount: result[:gross_payment_amount],
508
+ taxable_payment_amount: result[:taxable_payment_amount],
509
+ tax_amount: result[:tax_amount],
510
+ net_payment_amount: result[:net_payment_amount],
511
+ category: result[:category],
512
+ submission_prize_exempted: c_int_to_bool(result[:submission_prize_exempted]),
513
+ breakdown: read_struct_array(
514
+ result.pointer + WithholdingTaxStruct.offset_of(:breakdown_storage),
515
+ BreakdownStepStruct,
516
+ result[:breakdown_len]
517
+ ).map do |step|
518
+ BreakdownStepRecord.new(
519
+ label: read_fixed_string(step, :label, LABEL_LEN),
520
+ base_amount: step[:base_amount],
521
+ rate_numer: step[:rate_numer],
522
+ rate_denom: step[:rate_denom],
523
+ result: step[:result]
524
+ )
525
+ end
526
+ )
527
+ end
528
+
529
+ def call_with_error
530
+ error_buf = FFI::MemoryPointer.new(:char, ERROR_BUF_LEN)
531
+ error_buf.write_string("")
532
+ status = yield(error_buf)
533
+ return if status.zero?
534
+
535
+ message = error_buf.read_string
536
+ message = "j-law-c-ffi call failed with status #{status}" if message.empty?
537
+ raise RuntimeError, message
538
+ end
539
+
540
+ def bool_to_c_int(value)
541
+ value ? 1 : 0
542
+ end
543
+
544
+ def c_int_to_bool(value)
545
+ !value.zero?
546
+ end
547
+
548
+ def validate_u64(value, field)
549
+ validate_unsigned_integer(value, field, UINT64_MAX)
550
+ end
551
+
552
+ def validate_u32(value, field)
553
+ validate_unsigned_integer(value, field, UINT32_MAX)
554
+ end
555
+
556
+ def validate_u16(value, field)
557
+ validate_unsigned_integer(value, field, UINT16_MAX)
558
+ end
559
+
560
+ def validate_u8(value, field)
561
+ validate_unsigned_integer(value, field, UINT8_MAX)
562
+ end
563
+
564
+ def validate_unsigned_integer(value, field, max_value)
565
+ unless value.is_a?(Integer)
566
+ raise TypeError, "#{field} には Integer を指定してください (got #{value.class})"
567
+ end
568
+
569
+ if value.negative?
570
+ raise ArgumentError, "#{field} には 0 以上の値を指定してください"
571
+ end
572
+
573
+ if value > max_value
574
+ raise ArgumentError, "#{field} は #{max_value} 以下で指定してください"
575
+ end
576
+
577
+ value
578
+ end
579
+
580
+ def validate_date_parts(year, month, day)
581
+ validated_year = validate_u16(year, "year")
582
+ validated_month = validate_u8(month, "month")
583
+ validated_day = validate_u8(day, "day")
584
+ Date.new(validated_year, validated_month, validated_day)
585
+ [validated_year, validated_month, validated_day]
586
+ rescue Date::Error => e
587
+ raise ArgumentError,
588
+ "無効な日付です: #{format_date_parts(validated_year, validated_month, validated_day)} (#{e.message})"
589
+ end
590
+
591
+ def format_date_parts(year, month, day)
592
+ format("%04d-%02d-%02d", year, month, day)
593
+ end
594
+
595
+ def read_struct_array(base_pointer, struct_class, length, max_length: MAX_TIERS)
596
+ safe_length = length.clamp(0, max_length)
597
+ Array.new(safe_length) do |index|
598
+ struct_class.new(base_pointer + (index * struct_class.size))
599
+ end
600
+ end
601
+
602
+ def build_income_deduction_input_struct(input)
603
+ spouse = input[:spouse]
604
+ dependent = input.fetch(:dependent, {})
605
+ medical = input[:medical]
606
+ life_insurance = input[:life_insurance]
607
+ donation = input[:donation]
608
+ year, month, day = validate_date_parts(input[:year], input[:month], input[:day])
609
+
610
+ struct = IncomeDeductionInputStruct.new
611
+ struct[:total_income_amount] = validate_u64(input[:total_income_amount], "total_income_amount")
612
+ struct[:year] = year
613
+ struct[:month] = month
614
+ struct[:day] = day
615
+ struct[:has_spouse] = bool_to_c_int(!spouse.nil?)
616
+ struct[:spouse_total_income_amount] =
617
+ validate_u64(spouse&.fetch(:spouse_total_income_amount, 0) || 0, "spouse_total_income_amount")
618
+ struct[:spouse_is_same_household] = bool_to_c_int(spouse&.fetch(:is_same_household, false) || false)
619
+ struct[:spouse_is_elderly] = bool_to_c_int(spouse&.fetch(:is_elderly, false) || false)
620
+ struct[:dependent_general_count] = validate_u64(dependent.fetch(:general_count, 0), "dependent.general_count")
621
+ struct[:dependent_specific_count] = validate_u64(dependent.fetch(:specific_count, 0), "dependent.specific_count")
622
+ struct[:dependent_elderly_cohabiting_count] =
623
+ validate_u64(dependent.fetch(:elderly_cohabiting_count, 0), "dependent.elderly_cohabiting_count")
624
+ struct[:dependent_elderly_other_count] =
625
+ validate_u64(dependent.fetch(:elderly_other_count, 0), "dependent.elderly_other_count")
626
+ struct[:social_insurance_premium_paid] =
627
+ validate_u64(input.fetch(:social_insurance_premium_paid, 0), "social_insurance_premium_paid")
628
+ struct[:has_medical] = bool_to_c_int(!medical.nil?)
629
+ struct[:medical_expense_paid] =
630
+ validate_u64(medical&.fetch(:medical_expense_paid, 0) || 0, "medical.medical_expense_paid")
631
+ struct[:medical_reimbursed_amount] =
632
+ validate_u64(medical&.fetch(:reimbursed_amount, 0) || 0, "medical.reimbursed_amount")
633
+ struct[:has_life_insurance] = bool_to_c_int(!life_insurance.nil?)
634
+ struct[:life_new_general_paid_amount] =
635
+ validate_u64(life_insurance&.fetch(:new_general_paid_amount, 0) || 0, "life_insurance.new_general_paid_amount")
636
+ struct[:life_new_individual_pension_paid_amount] =
637
+ validate_u64(life_insurance&.fetch(:new_individual_pension_paid_amount, 0) || 0, "life_insurance.new_individual_pension_paid_amount")
638
+ struct[:life_new_care_medical_paid_amount] =
639
+ validate_u64(life_insurance&.fetch(:new_care_medical_paid_amount, 0) || 0, "life_insurance.new_care_medical_paid_amount")
640
+ struct[:life_old_general_paid_amount] =
641
+ validate_u64(life_insurance&.fetch(:old_general_paid_amount, 0) || 0, "life_insurance.old_general_paid_amount")
642
+ struct[:life_old_individual_pension_paid_amount] =
643
+ validate_u64(life_insurance&.fetch(:old_individual_pension_paid_amount, 0) || 0, "life_insurance.old_individual_pension_paid_amount")
644
+ struct[:has_donation] = bool_to_c_int(!donation.nil?)
645
+ struct[:donation_qualified_amount] =
646
+ validate_u64(donation&.fetch(:qualified_donation_amount, 0) || 0, "donation.qualified_donation_amount")
647
+ struct
648
+ end
649
+
650
+ def read_fixed_string(struct, field, length)
651
+ bytes = struct.pointer.get_bytes(struct.class.offset_of(field), length)
652
+ bytes.split("\x00", 2).first.force_encoding("UTF-8")
653
+ end
654
+
655
+ private_class_method :call_with_error, :bool_to_c_int, :c_int_to_bool,
656
+ :validate_u64, :validate_u32, :validate_u16, :validate_u8,
657
+ :validate_unsigned_integer, :validate_date_parts, :format_date_parts,
658
+ :read_struct_array, :read_fixed_string,
659
+ :build_income_deduction_input_struct
660
+ end
661
+ end
662
+ end