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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +202 -0
- data/lib/elia/mcc/active_model_validator.rb +50 -0
- data/lib/elia/mcc/category.rb +107 -0
- data/lib/elia/mcc/code.rb +242 -0
- data/lib/elia/mcc/collection.rb +276 -0
- data/lib/elia/mcc/configuration.rb +85 -0
- data/lib/elia/mcc/data/.gitkeep +0 -0
- data/lib/elia/mcc/data/mcc_codes.yml +12809 -0
- data/lib/elia/mcc/data/ranges.yml +126 -0
- data/lib/elia/mcc/data/risk_categories.yml +293 -0
- data/lib/elia/mcc/errors.rb +48 -0
- data/lib/elia/mcc/railtie.rb +36 -0
- data/lib/elia/mcc/range.rb +158 -0
- data/lib/elia/mcc/serializer.rb +89 -0
- data/lib/elia/mcc/validator.rb +102 -0
- data/lib/elia/mcc/version.rb +7 -0
- data/lib/elia/mcc.rb +65 -0
- data/lib/elia_ruby/version.rb +5 -0
- data/lib/elia_ruby.rb +35 -0
- metadata +98 -0
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
|