elia_ruby 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 49ae65cb7eaefca01091ebcc4f6b23a6030459ebc92d23e3d1cf89296ac91a01
4
+ data.tar.gz: d27b6d6b4418647ef1609f20cb6174eda07d83f33c6b96bf6112913823c7711a
5
+ SHA512:
6
+ metadata.gz: 31265e6712f5095ba8dd6329c468e3265df7a20e3db31a97134f8953395454dbecaec7a479d1aa745f409942958aa2597bf63c1c15550990eb78efd920cc8a89
7
+ data.tar.gz: d9a7358755aa6582481abe977820f4d160b8416b5865dd829c26cbcde70a849f73448f79c8be642b202f2f25d742252aa11cccdf47b2cadabfad588ecdf30f65
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Elia Pay SAS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # Elia Ruby
2
+
3
+ A comprehensive Ruby gem for working with Merchant Category Codes (MCC) in payment processing. Provides validation, categorization, risk assessment, and multi-source data aggregation.
4
+
5
+ ## Features
6
+
7
+ - **Multi-source MCC data** - Aggregates descriptions from ISO 18245, USDA, Stripe, Visa, Mastercard, American Express, Alipay, and IRS
8
+ - **Risk categories** - Pre-defined categories for payment control (gambling, airlines, adult, crypto, etc.)
9
+ - **ISO 18245 ranges** - Standard category ranges from the official specification
10
+ - **Fuzzy search** - Search across all descriptions to find relevant MCCs
11
+ - **Rails integration** - ActiveModel validators and serializers
12
+ - **IRS reportable flags** - Know which MCCs require 1099-K reporting
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'elia_ruby'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ gem install elia_ruby
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Basic Lookup
37
+
38
+ ```ruby
39
+ require 'elia_ruby'
40
+
41
+ # Find an MCC
42
+ code = Elia::Mcc.find("5411")
43
+ code.mcc # => "5411"
44
+ code.iso_description # => "Grocery Stores, Supermarkets"
45
+ code.stripe_code # => "grocery_stores_supermarkets"
46
+ code.irs_reportable? # => false
47
+
48
+ # Strict lookup (raises if not found)
49
+ Elia::Mcc.find!("5411") # => Elia::Mcc::Code
50
+ Elia::Mcc.find!("99999") # => raises Elia::Mcc::NotFound
51
+
52
+ # Shorthand
53
+ Elia::Mcc["5411"] # => same as find
54
+ ```
55
+
56
+ ### Collections and Queries
57
+
58
+ ```ruby
59
+ # Get all MCCs
60
+ Elia::Mcc.all # => Array of all codes
61
+
62
+ # Filter by attributes
63
+ Elia::Mcc.where(irs_reportable: true) # => IRS reportable codes
64
+
65
+ # Range queries
66
+ Elia::Mcc.in_range("5000", "5999") # => Retail MCCs
67
+
68
+ # Search descriptions
69
+ Elia::Mcc.search("restaurant") # => fuzzy search results
70
+ ```
71
+
72
+ ### Risk Categories
73
+
74
+ ```ruby
75
+ # Available categories
76
+ Elia::Mcc.categories # => [:airlines, :gambling, :adult, :crypto, ...]
77
+
78
+ # Get codes in a category
79
+ Elia::Mcc.in_category(:gambling) # => gambling-related MCCs
80
+ Elia::Mcc.in_category(:airlines) # => airline MCCs only
81
+
82
+ # Check a code's categories
83
+ code = Elia::Mcc.find("7995")
84
+ code.categories # => [:gambling]
85
+ code.in_category?(:gambling) # => true
86
+ ```
87
+
88
+ ### ISO 18245 Ranges
89
+
90
+ ```ruby
91
+ # Get all ranges
92
+ Elia::Mcc.ranges # => Array of Elia::Mcc::Range objects
93
+
94
+ # Check a code's range
95
+ code = Elia::Mcc.find("5411")
96
+ code.range.name # => "Retail Outlets"
97
+ ```
98
+
99
+ ### Validation
100
+
101
+ ```ruby
102
+ # Check if valid
103
+ Elia::Mcc.valid?("5411") # => true
104
+ Elia::Mcc.valid?("99999") # => false
105
+ ```
106
+
107
+ ### Rails Integration
108
+
109
+ #### ActiveModel Validator
110
+
111
+ ```ruby
112
+ class Transaction < ApplicationRecord
113
+ validates :mcc_code, mcc: true
114
+
115
+ # Or with category restrictions
116
+ validates :mcc_code, mcc: {
117
+ deny_categories: [:gambling, :adult],
118
+ message: "category is blocked"
119
+ }
120
+ end
121
+ ```
122
+
123
+ #### Configuration
124
+
125
+ ```ruby
126
+ # config/initializers/elia_mcc.rb
127
+ Elia::Mcc.configure do |config|
128
+ config.default_description_source = :stripe # or :iso, :visa, etc.
129
+ config.include_reserved_ranges = false
130
+ end
131
+ ```
132
+
133
+ ### Code Attributes
134
+
135
+ ```ruby
136
+ code = Elia::Mcc.find("5411")
137
+
138
+ # Core
139
+ code.mcc # => "5411"
140
+
141
+ # Multi-source descriptions
142
+ code.iso_description
143
+ code.usda_description
144
+ code.stripe_description
145
+ code.stripe_code # => programmatic identifier
146
+ code.visa_description
147
+ code.visa_clearing_name
148
+ code.mastercard_description
149
+ code.amex_description
150
+ code.alipay_description
151
+
152
+ # IRS
153
+ code.irs_description
154
+ code.irs_reportable?
155
+
156
+ # Categorization
157
+ code.range # => Elia::Mcc::Range
158
+ code.categories # => Array of symbols
159
+
160
+ # Serialization
161
+ code.to_h # => Hash with all attributes
162
+ code.as_json # => JSON-ready hash
163
+ ```
164
+
165
+ ## Data Sources
166
+
167
+ This gem aggregates MCC data from multiple authoritative sources:
168
+
169
+ - **ISO 18245:2023** - The official international standard for MCC definitions
170
+ - **USDA** - Comprehensive list including private-use ranges
171
+ - **Stripe** - Descriptions with programmatic codes
172
+ - **Visa** - Descriptions with clearing names
173
+ - **Mastercard** - Descriptions with abbreviated names
174
+ - **American Express** - Descriptions
175
+ - **Alipay** - Descriptions
176
+ - **IRS** - Reportable flags for 1099-K compliance
177
+
178
+ ## Inspiration and Credits
179
+
180
+ This gem was inspired by and incorporates ideas from several excellent projects:
181
+
182
+ - [python-iso18245](https://github.com/alubbock/python-iso18245) - Comprehensive Python MCC library with multi-source data aggregation
183
+ - [mcc-ruby](https://github.com/singlebrook/mcc-ruby) - Ruby gem with IRS reportable data
184
+ - [mcc](https://github.com/maximbilan/mcc) - MCC data collection by Maxim Bilan
185
+
186
+ ## Development
187
+
188
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt.
189
+
190
+ ```bash
191
+ bundle install
192
+ bundle exec rspec
193
+ bundle exec rubocop
194
+ ```
195
+
196
+ ## Contributing
197
+
198
+ Bug reports and pull requests are welcome on GitHub at https://github.com/eliapay/elia-ruby.
199
+
200
+ ## License
201
+
202
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ # ActiveModel validator for MCC codes
6
+ #
7
+ # @example Basic usage
8
+ # class Transaction < ApplicationRecord
9
+ # validates :mcc_code, mcc: true
10
+ # end
11
+ #
12
+ # @example With category restrictions
13
+ # class Transaction < ApplicationRecord
14
+ # validates :mcc_code, mcc: { deny_categories: [:gambling, :adult] }
15
+ # end
16
+ #
17
+ # @example With custom message
18
+ # class Transaction < ApplicationRecord
19
+ # validates :mcc_code, mcc: {
20
+ # deny_categories: [:gambling],
21
+ # message: "category is blocked"
22
+ # }
23
+ # end
24
+ class MccValidator < ActiveModel::EachValidator
25
+ # Validates an attribute on a record
26
+ #
27
+ # @param record [Object] the record being validated
28
+ # @param attribute [Symbol] the attribute name
29
+ # @param value [Object] the attribute value
30
+ def validate_each(record, attribute, value)
31
+ return if value.blank? && options[:allow_blank]
32
+
33
+ validator = Elia::Mcc::Validator.new(validator_options)
34
+ errors = validator.validate(value)
35
+
36
+ errors.each do |message|
37
+ record.errors.add(attribute, options[:message] || message)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def validator_options
44
+ {
45
+ strict: options.fetch(:strict, true),
46
+ deny_categories: Array(options[:deny_categories]),
47
+ allow_categories: options[:allow_categories],
48
+ }
49
+ end
50
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elia
4
+ module Mcc
5
+ # Represents a risk category for MCC codes
6
+ #
7
+ # Categories help organize MCC codes by risk level, industry type,
8
+ # or other business classification criteria for payment controls.
9
+ class Category
10
+ # @return [Symbol] unique identifier for this category
11
+ attr_reader :id
12
+
13
+ # @return [String] human-readable name
14
+ attr_reader :name
15
+
16
+ # @return [String] detailed description
17
+ attr_reader :description
18
+
19
+ # @return [Array<String>] array of codes and ranges (e.g., "7995" or "3000-3350")
20
+ attr_reader :codes
21
+
22
+ # Creates a new Category instance
23
+ #
24
+ # @param attributes [Hash] the category attributes
25
+ # @option attributes [String, Symbol] :id unique identifier
26
+ # @option attributes [String] :name human-readable name
27
+ # @option attributes [String] :description detailed description
28
+ # @option attributes [Array<String>] :codes array of codes and ranges
29
+ def initialize(attributes = {})
30
+ attributes = attributes.transform_keys(&:to_sym)
31
+
32
+ @id = attributes[:id].to_sym
33
+ @name = attributes[:name].to_s
34
+ @description = attributes[:description].to_s
35
+ @codes = Array(attributes[:codes]).map(&:to_s)
36
+ end
37
+
38
+ # Returns whether the given MCC code is in this category
39
+ # Supports individual codes ("7995") and ranges ("3000-3350")
40
+ #
41
+ # @param mcc [String, Integer, Code] the code to check
42
+ # @return [Boolean] true if the code is in this category
43
+ def include?(mcc)
44
+ code = mcc.respond_to?(:mcc) ? mcc.mcc : mcc
45
+ normalized = code.to_s.strip.rjust(4, "0")
46
+
47
+ codes.any? do |entry|
48
+ if entry.include?("-")
49
+ # Range entry like "3000-3350"
50
+ start_code, end_code = entry.split("-").map { |c| c.strip.rjust(4, "0") }
51
+ normalized.between?(start_code, end_code)
52
+ else
53
+ # Individual code like "7995"
54
+ entry.strip.rjust(4, "0") == normalized
55
+ end
56
+ end
57
+ end
58
+
59
+ alias cover? include?
60
+
61
+ # Returns whether this category equals another
62
+ #
63
+ # @param other [Category] the category to compare
64
+ # @return [Boolean] true if the categories are equal
65
+ def ==(other)
66
+ return false unless other.is_a?(self.class)
67
+
68
+ id == other.id
69
+ end
70
+
71
+ alias eql? ==
72
+
73
+ # Returns a hash code for this instance
74
+ #
75
+ # @return [Integer] the hash code
76
+ def hash
77
+ id.hash
78
+ end
79
+
80
+ # Returns a human-readable representation
81
+ #
82
+ # @return [String] the inspection string
83
+ def inspect
84
+ "#<#{self.class.name} id=#{id.inspect} name=#{name.inspect} codes_count=#{codes.size}>"
85
+ end
86
+
87
+ # Returns the category as a string
88
+ #
89
+ # @return [String] the category name
90
+ def to_s
91
+ name
92
+ end
93
+
94
+ # Returns a hash representation
95
+ #
96
+ # @return [Hash] the category as a hash
97
+ def to_h
98
+ {
99
+ id: id,
100
+ name: name,
101
+ description: description,
102
+ codes: codes,
103
+ }
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elia
4
+ module Mcc
5
+ # Represents a single Merchant Category Code (MCC)
6
+ #
7
+ # MCC codes are 4-digit numbers used to classify businesses
8
+ # by the type of goods or services they provide.
9
+ class Code
10
+ # @return [String] the 4-digit MCC code
11
+ attr_reader :mcc
12
+
13
+ # @return [String, nil] Official ISO 18245 description
14
+ attr_reader :iso_description
15
+
16
+ # @return [String, nil] USDA description
17
+ attr_reader :usda_description
18
+
19
+ # @return [String, nil] Stripe API description
20
+ attr_reader :stripe_description
21
+
22
+ # @return [String, nil] Stripe's snake_case identifier
23
+ attr_reader :stripe_code
24
+
25
+ # @return [String, nil] Visa description
26
+ attr_reader :visa_description
27
+
28
+ # @return [String, nil] Visa's abbreviated clearing name
29
+ attr_reader :visa_clearing_name
30
+
31
+ # @return [String, nil] Mastercard description
32
+ attr_reader :mastercard_description
33
+
34
+ # @return [String, nil] American Express description
35
+ attr_reader :amex_description
36
+
37
+ # @return [String, nil] Alipay description
38
+ attr_reader :alipay_description
39
+
40
+ # @return [String, nil] IRS description for tax reporting
41
+ attr_reader :irs_description
42
+
43
+ # @return [Boolean, nil] Whether transactions are reportable under IRS 6050W
44
+ attr_reader :irs_reportable
45
+
46
+ # Description source mapping
47
+ DESCRIPTION_FIELDS = {
48
+ iso: :iso_description,
49
+ usda: :usda_description,
50
+ stripe: :stripe_description,
51
+ visa: :visa_description,
52
+ mastercard: :mastercard_description,
53
+ amex: :amex_description,
54
+ alipay: :alipay_description,
55
+ irs: :irs_description,
56
+ }.freeze
57
+
58
+ # Creates a new Code instance
59
+ #
60
+ # @param attributes [Hash] the code attributes
61
+ # @option attributes [String, Integer] :mcc the 4-digit MCC code
62
+ # @option attributes [String] :iso_description Official ISO 18245 description
63
+ # @option attributes [String] :usda_description USDA description
64
+ # @option attributes [String] :stripe_description Stripe API description
65
+ # @option attributes [String] :stripe_code Stripe's snake_case identifier
66
+ # @option attributes [String] :visa_description Visa description
67
+ # @option attributes [String] :visa_clearing_name Visa's clearing name
68
+ # @option attributes [String] :mastercard_description Mastercard description
69
+ # @option attributes [String] :amex_description American Express description
70
+ # @option attributes [String] :alipay_description Alipay description
71
+ # @option attributes [String] :irs_description IRS description
72
+ # @option attributes [Boolean] :irs_reportable Whether IRS reportable
73
+ # @raise [InvalidCode] if the code format is invalid
74
+ def initialize(attributes = {})
75
+ attributes = attributes.transform_keys(&:to_sym)
76
+
77
+ @mcc = normalize_code(attributes[:mcc])
78
+ @iso_description = attributes[:iso_description]&.to_s.presence
79
+ @usda_description = attributes[:usda_description]&.to_s.presence
80
+ @stripe_description = attributes[:stripe_description]&.to_s.presence
81
+ @stripe_code = attributes[:stripe_code]&.to_s.presence
82
+ @visa_description = attributes[:visa_description]&.to_s.presence
83
+ @visa_clearing_name = attributes[:visa_clearing_name]&.to_s.presence
84
+ @mastercard_description = attributes[:mastercard_description]&.to_s.presence
85
+ @amex_description = attributes[:amex_description]&.to_s.presence
86
+ @alipay_description = attributes[:alipay_description]&.to_s.presence
87
+ @irs_description = attributes[:irs_description]&.to_s.presence
88
+ @irs_reportable = attributes[:irs_reportable]
89
+ end
90
+
91
+ # Returns whether this code is IRS reportable
92
+ #
93
+ # @return [Boolean] true if the code is IRS reportable
94
+ def irs_reportable?
95
+ @irs_reportable == true
96
+ end
97
+
98
+ # Returns the description based on the configured default source
99
+ # Falls back through sources if the default is blank
100
+ #
101
+ # @param source [Symbol, nil] override the default source
102
+ # @return [String, nil] the description
103
+ def description(source: nil)
104
+ source ||= Elia::Mcc.configuration.default_description_source
105
+
106
+ # Try the requested source first
107
+ field = DESCRIPTION_FIELDS[source]
108
+ result = send(field) if field
109
+
110
+ return result if result.present?
111
+
112
+ # Fall back through other sources in order
113
+ DESCRIPTION_FIELDS.each_value do |field_name|
114
+ result = send(field_name)
115
+ return result if result.present?
116
+ end
117
+
118
+ nil
119
+ end
120
+
121
+ # Returns the ISO 18245 range this code belongs to
122
+ #
123
+ # @return [Range, nil] the range containing this code
124
+ def range
125
+ Elia::Mcc.ranges.find { |r| r.include?(mcc) }
126
+ end
127
+
128
+ # Returns all categories this code belongs to
129
+ #
130
+ # @return [Array<Category>] the categories containing this code
131
+ def categories
132
+ Elia::Mcc.categories.select { |c| c.include?(mcc) }
133
+ end
134
+
135
+ # Returns whether this code is in the given category
136
+ #
137
+ # @param category [Category, Symbol, String] the category to check
138
+ # @return [Boolean] true if in the category
139
+ def in_category?(category)
140
+ category_id = case category
141
+ when Category then category.id
142
+ when Symbol then category
143
+ else category.to_s.to_sym
144
+ end
145
+
146
+ Elia::Mcc.in_category(category_id).any? { |c| c.mcc == mcc }
147
+ end
148
+
149
+ # Returns whether this code matches the given value
150
+ #
151
+ # @param other [String, Integer, Code] the value to compare
152
+ # @return [Boolean] true if the codes match
153
+ def ==(other)
154
+ case other
155
+ when Code
156
+ mcc == other.mcc
157
+ when String, Integer
158
+ mcc == normalize_code(other)
159
+ else
160
+ false
161
+ end
162
+ end
163
+
164
+ alias eql? ==
165
+
166
+ # Returns a hash code for this instance
167
+ #
168
+ # @return [Integer] the hash code
169
+ def hash
170
+ mcc.hash
171
+ end
172
+
173
+ # Returns the code as an integer
174
+ #
175
+ # @return [Integer] the numeric value of the code
176
+ def to_i
177
+ mcc.to_i
178
+ end
179
+
180
+ # Returns the code as a string
181
+ #
182
+ # @return [String] the 4-digit code string
183
+ def to_s
184
+ mcc
185
+ end
186
+
187
+ # Returns a human-readable representation
188
+ #
189
+ # @return [String] the inspection string
190
+ def inspect
191
+ "#<#{self.class.name} mcc=#{mcc.inspect} description=#{description.inspect}>"
192
+ end
193
+
194
+ # Returns a hash representation of all attributes
195
+ #
196
+ # @return [Hash] all attributes as a hash
197
+ def to_h
198
+ {
199
+ mcc: mcc,
200
+ iso_description: iso_description,
201
+ usda_description: usda_description,
202
+ stripe_description: stripe_description,
203
+ stripe_code: stripe_code,
204
+ visa_description: visa_description,
205
+ visa_clearing_name: visa_clearing_name,
206
+ mastercard_description: mastercard_description,
207
+ amex_description: amex_description,
208
+ alipay_description: alipay_description,
209
+ irs_description: irs_description,
210
+ irs_reportable: irs_reportable,
211
+ }
212
+ end
213
+
214
+ # Returns a JSON-compatible hash representation
215
+ #
216
+ # @param options [Hash] JSON options (unused, for compatibility)
217
+ # @return [Hash] JSON-compatible hash
218
+ def as_json(_options = {})
219
+ to_h.merge(
220
+ description: description,
221
+ categories: categories.map(&:id),
222
+ range: range&.name
223
+ )
224
+ end
225
+
226
+ private
227
+
228
+ # Normalizes a code value to a 4-digit string
229
+ #
230
+ # @param value [String, Integer] the value to normalize
231
+ # @return [String] the normalized 4-digit string
232
+ # @raise [InvalidCode] if the value cannot be normalized
233
+ def normalize_code(value)
234
+ normalized = value.to_s.strip.rjust(4, "0")
235
+
236
+ raise InvalidCode, value unless normalized.match?(/\A\d{4}\z/)
237
+
238
+ normalized
239
+ end
240
+ end
241
+ end
242
+ end