sunnyside 0.0.5 → 0.0.7

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.
@@ -1,216 +1,171 @@
1
1
  module Sunnyside
2
2
  def self.edi_parser
3
3
  print "checking for new files...\n"
4
- Dir["#{LOCAL_FILES}/835/*.txt"].select { |file| Filelib.where(:filename => file).count == 0 }.each do |file|
4
+ Dir["#{DRIVE}/sunnyside-files/835/*.txt"].each do |file|
5
5
  print "processing #{file}...\n"
6
- data = File.open(file).read
7
-
6
+ file_data = File.open(file)
7
+ data = file_data.read
8
8
  # Detect to see if the EDI file already has new lines inserted. If so, the newlines are removed before the file gets processed.
9
9
 
10
- if data.include?(/\n/)
11
- data.gsub!(/\n/, '')
12
- end
10
+ data.gsub!(/\n/, '')
13
11
 
14
- data = data.split(/~CLP\*/)
15
- edi = Edi.new(data, file)
16
- edi.parse_claim_header
17
- Filelib.insert(filename: file, created_at: Time.now, purpose: 'EDI Import', file_type: '835 Remittance')
18
- edi.save_payment_to_db
12
+ data = data.split(/~CLP\*/)
13
+
14
+ edi_file = EdiReader.new(data)
15
+ edi_file.parse_claims
16
+ Filelib.insert(filename: file, purpose: '835')
17
+ file_data.close
18
+ FileUtils.mv(file, "#{DRIVE}/sunnyside-files/835/archive/#{File.basename(file)}")
19
19
  end
20
20
  end
21
21
 
22
- class Edi
23
- attr_reader :header, :claims, :file
24
- def initialize(data, file)
25
- @header, @claims = data[0], data.drop(1)
26
- @file = file
22
+ class EdiReader
23
+ attr_reader :data
24
+
25
+ def initialize(data)
26
+ @data = data
27
27
  end
28
28
 
29
- def process_file
30
- claims.map { |clm| clm.split(/~(?=SVC)/) }.each do |claim|
31
- claim_head = claim[0]
32
- services = claim.select { |section| section =~ /^SVC/ }
33
- InvoiceHeader.new(claim_head, services).parse_data
34
- end
29
+ def claims
30
+ data.select { |clm| clm =~ /^\d+/ }.map { |clm| clm.split(/~(?=SVC)/) }
35
31
  end
36
32
 
33
+
34
+
37
35
  def check_number
38
- header[/(?<=~TRN\*\d\*)\w+/, 0]
36
+ check = data[0][/(?<=~TRN\*\d\*)\w+/]
37
+ return check.include?('E') ? check[/\d+$/] : check
39
38
  end
40
39
 
41
40
  def check_total
42
- header[/(?<=~BPR\*\w\*)[0-9\.\-]+/] || 0.0
41
+ data[0][/(?<=~BPR\*\w\*)[0-9\.\-]+/, 0]
43
42
  end
44
43
 
45
- def type
46
- if header[/(?<=\*C\*)ACH/] == 'ACH'
47
- 'Electronic Funds Transfer'
48
- elsif header[/(?<=\*C\*)CHK/] == 'CHK'
49
- 'Physical Check Issued'
50
- else
51
- 'Non Payment'
52
- end
44
+ def parse_claims
45
+ payment_id = Payment.insert(check_number: check_number, check_total: check_total)
46
+ claims.each { |claim| ClaimParser.new(claim, payment_id).parse }
53
47
  end
48
+ end
54
49
 
55
- # not working
50
+ class ClaimParser < EdiReader
51
+ attr_reader :claim_header, :service_data, :payment_id
56
52
 
57
- # def check
58
- # if check_number.include?('E') # E for Fidelis
59
- # check_number[/\d+[A-Z]+(\d+)/, 1]
60
- # else
61
- # check_number
62
- # end
63
- # end
53
+ def initialize(claim, payment_id)
54
+ @claim_header = claim[0].split(/\*/)
55
+ @service_data = claim.select { |clm| clm =~ /^SVC/ }
56
+ @payment_id = Payment[payment_id]
57
+ end
64
58
 
65
- def separate_claims_from_services
66
- claims.map {|clm| clm.split(/~(?=SVC)/)}
59
+ def header
60
+ {
61
+ :invoice => claim_header[0],
62
+ :response_code => claim_header[1],
63
+ :billed => claim_header[2],
64
+ :paid => claim_header[3],
65
+ :units => claim_header[5], # 4 is not used - that is the patient responsibility amount
66
+ :claim_number => claim_header[6][/^\d+/]
67
+ }
67
68
  end
68
69
 
69
- def parse_claim_header
70
- separate_claims_from_services.each do |clm|
71
- claim_data = clm[0]
72
- services = clm.reject{|x| x !~ /^SVC/}
73
- claims = InvoiceHeader.new(claim_data, check_number)
74
- claims.format_data
75
- claims.add_to_db(check_number, file)
76
- parse_service(services) {|svc| claims.parse_svc(svc, check_number)}
77
- end
70
+ def parse
71
+ claim = ClaimEntry.new(header)
72
+ claim_id = claim.to_db(payment_id)
73
+ service_data.each { |service| ServiceParser.new(service, claim_id).parse }
78
74
  end
75
+ end
79
76
 
80
- def save_payment_to_db
81
- provider = Claim.where(check_number: check_number).get(:provider_id) || 17
82
- Payment.insert(provider_id: provider, filelib_id: filelib_id, check_total: check_total, check_number: check_number)
77
+ class ClaimEntry < EdiReader
78
+ attr_reader :invoice, :response_code, :billed, :paid, :units, :claim_number
79
+
80
+ def initialize(header = {})
81
+ @invoice = header[:invoice].gsub(/[OLD]/, 'O' => '0', 'D' => '8', 'L' => '1').gsub(/^0/, '')[0..5].to_i # for the corrupt SHP EDI files
82
+ @response_code = header[:response_code]
83
+ @billed = header[:billed]
84
+ @paid = header[:paid]
85
+ @units = header[:units]
86
+ @claim_number = header[:claim_number]
83
87
  end
84
88
 
85
- def filelib_id
86
- Filelib.where(filename: file).get(:id)
89
+ def to_db(payment)
90
+ payment.update(provider_id: inv.provider_id) if payment.provider_id.nil?
91
+ Claim.insert(
92
+ :invoice_id => invoice,
93
+ :payment_id => payment.id,
94
+ :client_id => inv.client_id,
95
+ :control_number => claim_number,
96
+ :paid => paid,
97
+ :billed => billed,
98
+ :status => response_code,
99
+ :provider_id => inv.provider_id,
100
+ :recipient_id => inv.recipient_id
101
+ )
87
102
  end
88
103
 
89
- def parse_service(services)
90
- services.map{|x| x.split(/~/).reject{|x| x !~ /CAS|SVC|DTM/}}.each {|svc| yield svc}
104
+ def inv
105
+ Invoice[invoice]
91
106
  end
92
107
  end
93
108
 
94
- class InvoiceHeader < Edi
95
- attr_accessor :claim_number, :invoice_number, :response_code, :amt_charged, :amt_paid, :check_number
96
- def initialize(claim, check_number)
97
- @invoice_number, @response_code, @amt_charged, @amt_paid, @whatever, @claim_number = claim.match(/^([\w\.]+)\*(\d+)\*([0-9\.\-]+)\*([0-9\.\-]+)\*([0-9\.\-]+)?\*+\w+\*(\w+)/).captures
98
- @check_number = check_number
99
- end
109
+ class ServiceParser < EdiReader
110
+ attr_reader :service_line, :claim
100
111
 
101
- def format_data
102
- @invoice_number = invoice_number[/^\w+/].gsub(/[OLD]/, 'O' => '0', 'D' => '8', 'L' => '1').gsub(/^0/, '')[0..5].to_i
112
+ def initialize(service_line, claim_id)
113
+ @service_line = service_line.split(/~/)
114
+ @claim = Claim[claim_id]
103
115
  end
104
116
 
105
- def claim_id
106
- Claim.where(invoice_id: invoice_number, check_number: check_number).get(:id)
117
+ def dos
118
+ service_line.detect { |svc| svc =~ /^DTM/ }[/\w+$/]
107
119
  end
108
120
 
109
- def parse_svc(service, check_number)
110
- if service.length == 2
111
- svc = Detail.new(service[0], service[1])
112
- elsif service.length > 2
113
- svc = Detail.new(service[0], service[1])
114
- svc.set_denial(service[2])
121
+ def service_header
122
+ line = service_line[0].split(/\*/).drop(1)
123
+ if line.length == 7 || line[1] != line[2]
124
+ return line.uniq
125
+ else
126
+ return line
115
127
  end
116
- svc.display(invoice_number)
117
- svc.save_to_db(invoice_number, check_number, claim_id)
118
128
  end
119
129
 
120
- def prov
121
- Invoice.where(invoice_number: invoice_number).get(:provider_id)
130
+ def error_codes
131
+ service_line.find_all { |section| section =~ /^CAS|^SE/ }.map { |code| code[/\w+\*\w+\*(\d+)/, 1] }
122
132
  end
123
133
 
124
- def add_to_db(check, file)
125
- Claim.insert(provider_id: prov, invoice_id: invoice_number, billed: amt_charged, paid: amt_paid, check_number: check_number, control_number: claim_number, status: response_code)
134
+ def parse
135
+ service = ServiceEntry.new(service_header, dos, error_codes)
136
+ service.to_db(claim)
126
137
  end
127
138
  end
128
139
 
129
- class Detail < Edi
130
-
131
- attr_reader :billed, :paid, :denial_reason, :date, :billed, :paid, :units, :service_code
132
- def initialize(service, date, denial_reason=nil, denial_code=nil)
133
- @service_code, @billed, @paid, @units = service.match(/HC:([A-Z0-9\:]+)\*([0-9\.\-]+)\*([0-9\.\-]+)?\**([0-9\-]+)?/).captures
134
- @date = Date.parse(date[/\d+$/])
135
- end
136
-
137
- def display(inv)
138
- print "#{inv} #{@service_code} #{@date} #{client(inv)} #{denial}\n"
139
- end
140
-
141
- def client(inv)
142
- Invoice.where(invoice_number: inv).get(:client_name)
143
- end
144
-
145
- def denial
146
- (billed.to_f - paid.to_f).round(2) if billed > paid
147
- end
148
-
149
- def set_denial(denial)
150
- @denial_reason = set_code(denial[/\d+/])
151
- end
152
-
153
- def save_to_db(invoice, check, claim_id)
154
- Service.insert(invoice_id: invoice, service_code: service_code, units: units.to_f, billed: billed.to_f, paid: paid.to_f, denial_reason: denial_reason, dos: date, claim_id: claim_id)
155
- end
156
-
157
- def set_code(code)
158
- case code
159
- when '125'
160
- 'Submission/billing error(s). At least one Remark Code must be provided'
161
- when '140'
162
- 'Patient/Insured health identification number and name do not match.'
163
- when '31'
164
- 'INVALID MEMBER ID'
165
- when '62'
166
- 'PAID AUTHORIZED UNITS'
167
- when '96'
168
- 'NO AUTHORIZATION FOR DOS'
169
- when '146'
170
- 'DIAGNOSIS WAS INVALID FOR DATES LISTED'
171
- when '197'
172
- 'Precertification/authorization/notification absent'
173
- when '198'
174
- 'Precertification/authorization exceeded'
175
- when '199'
176
- 'Revenue code and Procedure code do not match'
177
- when '9'
178
- 'DIAGNOSIS ISSUE'
179
- when '15'
180
- 'AUTHORIZATION MISSING/INVALID'
181
- when '18'
182
- 'Exact Duplicate Claim/Service'
183
- when '19'
184
- 'Expenses incurred prior to coverage'
185
- when '27'
186
- 'Expenses incurred after coverage terminated'
187
- when '29'
188
- 'Timely Filing'
189
- when '39'
190
- 'Services denied at the time authorization/pre-certification was requested'
191
- when '45'
192
- 'Charge exceeds fee schedule/maximum allowable'
193
- when '16'
194
- 'Claim/service lacks information which is needed for adjudication'
195
- when '50'
196
- 'These are non-covered services because this is not deemed a medical necessity by the payer'
197
- when '192'
198
- 'Non standard adjustment code from paper remittance'
199
- when '181'
200
- 'Procedure code was invalid on the date of service'
201
- when '182'
202
- 'Procedure modifier was invalid on the date of service'
203
- when '204'
204
- 'This service/equipment/drug is not covered under the patients current benefit plan'
205
- when '151'
206
- '151 Payment adjusted because the payer deems the information submitted does not support this many/frequency of services'
207
- when '177'
208
- 'Patient has not met the required eligibility requirements'
209
- when '109'
210
- 'Claim/service not covered by this payer/contractor. You must send the claim/service to the correct payer/contractor.'
211
- else
212
- "#{code} is UNIDENTIFIED"
213
- end
140
+ class ServiceEntry < EdiReader
141
+ attr_reader :paid, :billed, :service_code, :units, :res_code, :error_codes, :dos
142
+
143
+ def initialize(service_header, dos, error_codes)
144
+ @service_code, @billed, @paid, @res_code, @units = service_header
145
+ @dos = Date.parse(dos)
146
+ @error_codes = error_codes.map { |id| Denial[id].denial_explanation }
147
+ end
148
+
149
+ def to_db(claim)
150
+ Service.insert(
151
+ :claim_id => claim.id,
152
+ :invoice_id => claim.invoice_id,
153
+ :payment_id => claim.payment_id,
154
+ :denial_reason => denial_reason,
155
+ :service_code => service_code.gsub(/HC:/, ''),
156
+ :paid => paid,
157
+ :billed => billed,
158
+ :units => units,
159
+ :dos => dos
160
+ )
161
+ end
162
+
163
+ def denial_reason
164
+ error_codes.join("\n") if denied?
165
+ end
166
+
167
+ def denied?
168
+ paid != billed
214
169
  end
215
170
  end
216
171
  end
@@ -2,17 +2,20 @@ require 'prawn'
2
2
  module Sunnyside
3
3
  # This should be redone.
4
4
  def self.ledger_file
5
- Dir["#{LOCAL_FILES}/summary/*.PDF", "#{LOCAL_FILES}/summary/*.pdf"].each {|file|
5
+ Dir["#{DRIVE}/sunnyside-files/summary/*.PDF"].each {|file|
6
6
  if Filelib.where(filename: file).count == 0
7
7
  puts "processing #{file}..."
8
8
  ledger = Ledger.new(file)
9
9
  ledger.process_file
10
10
  Filelib.insert(filename: file, purpose: 'summary')
11
+ FileUtils.mv(file, "#{DRIVE}/sunnyside-files/summary/archive/#{File.basename(file)}")
12
+ ledger.export_to_csv
11
13
  end
12
14
  }
13
15
  end
14
16
 
15
17
  class Ledger
18
+ include Sunnyside
16
19
  attr_reader :post_date, :file, :pages
17
20
 
18
21
  # when Ledger gets initialized, the page variable filters out the VNS clients
@@ -23,12 +26,17 @@ module Sunnyside
23
26
  @pages = PDF::Reader.new(file).pages.select { |page| !page.raw_content.include?('VISITING NURSE SERVICE') }
24
27
  end
25
28
 
26
- def providers
27
- pages.map { |page| PageData.new(page.raw_content, file) }
29
+ def process_file
30
+ pages.each { |page| PageData.new(page.raw_content, post_date).invoice_data }
28
31
  end
29
32
 
30
- def process_file
31
- providers.each { |page| page.invoice_data }
33
+ def post_date
34
+ Date.parse(file[0..7])
35
+ end
36
+
37
+ def export_to_csv
38
+ CSV.open("#{DRIVE}/sunnyside-files/new-ledger/#{inv.post_date}-IMPORT-FUND-EZ-LEDGER.csv", "a+") { |row| row << ['Seq','inv','post_date','other id','prov','invoice','header memo','batch','doc date','detail memo','fund','account','cc1','cc2','cc3','debit','credit'] }
39
+ Invoice.where(post_date: post_date).all.each { |inv| self.payable_csv(inv, post_date) }
32
40
  end
33
41
  end
34
42
 
@@ -37,13 +45,12 @@ module Sunnyside
37
45
  # Then, the data gets finalized (via the InvoiceLine child class of PageData) and inserted into the database.
38
46
 
39
47
  class PageData
40
- include Sunnyside
41
- attr_reader :page_data, :provider, :post_date
48
+ attr_reader :page_data, :provider, :post_date, :invoice_data
42
49
 
43
- def initialize(page_data, file)
50
+ def initialize(page_data, post_date)
44
51
  @provider = page_data[/CUSTOMER:\s+(.+)(?=\)')/, 1]
45
- @post_date = Date.parse(file[0..7])
46
- @page_data = page_data.split(/\n/).select { |line| line =~ /^\([0-9\/]+\s/ }
52
+ @post_date = post_date
53
+ @page_data = page_data.split(/\n/).select { |line| line =~ /^\([0-9\/]{8}\s/ }
47
54
  end
48
55
 
49
56
  # Since the source data is somewhat unreliable in the format, there have been two different variations of AMERIGROUP and ELDERSERVE.
@@ -69,30 +76,33 @@ module Sunnyside
69
76
  end
70
77
 
71
78
  def invoice_lines
72
- page_data.map { |line| InvoiceLine.new(line, formatted_provider, post_date) }
79
+ page_data.map { |line|
80
+ line.gsub!(/^\(|\)'/)
81
+ client_name = line.slice!(20..45)
82
+ line = InvoiceLine.new(line, formatted_provider, post_date, client_name)
83
+ }
73
84
  end
74
85
 
75
86
  def invoice_data
76
87
  invoice_lines.each { |inv| inv.finalize }
77
- # Invoice.where(post_date: post_date).all.each { |inv| self.payable_csv(inv, post_date, formatted_provider) }
78
88
  end
79
89
 
80
90
  # InvoiceLine does all the nitty-gritty parsing of an invoice line into the necessary fields the DB requres.
81
91
 
82
92
  class InvoiceLine < PageData
83
93
  attr_accessor :invoice, :rate, :hours, :amount, :client_id, :client_name, :post_date, :provider
84
- def initialize(line, provider, post_date)
85
- @provider = provider
86
- @post_date = post_date
87
- @client_name = line.slice!(20..45)
94
+ def initialize(line, provider, post_date, client_name)
88
95
  @doc_date, @invoice, @client_id, @hours, @rate, @amount = line.split
96
+ @post_date = post_date
97
+ @client_name = client_name.strip
98
+ @provider = provider
89
99
  end
90
100
 
91
101
  # Some invoice totals exceed $999.99, so the strings need to be parsed into a format, sans comma, that the DB will read correctly.
92
102
  # Otherwise, the DB will read 1,203.93 as 1.0.
93
103
 
94
104
  def amt
95
- amount.gsub(/,/, '')
105
+ amount.gsub(/,|\)'/, '')
96
106
  end
97
107
 
98
108
  # Ocasionally, new clients will appear on the PDF doc. If the DB does not find a client with the client_id, then it executes a method wherein
@@ -100,11 +110,11 @@ module Sunnyside
100
110
 
101
111
  def finalize
102
112
  if !client_missing?
103
- add_invoice
104
113
  update_client if new_provider?
114
+ add_invoice
105
115
  else
106
116
  add_client
107
- finalize
117
+ add_invoice
108
118
  end
109
119
  end
110
120
 
@@ -117,16 +127,16 @@ module Sunnyside
117
127
  end
118
128
 
119
129
  def update_client
120
- Client[client_id].update(provider_id: provider.id, type: provider.type)
130
+ Client[client_id].update(provider_id: provider.id, prov_type: provider.prov_type)
121
131
  end
122
132
 
123
133
  def fund_id
124
- print "Enter in the FUND EZ ID for this client: #{client_name.strip} of #{provider.name}. "
134
+ print "Enter in the FUND EZ ID for this client: #{client_name} of #{provider.name}: "
125
135
  return gets.chomp
126
136
  end
127
137
 
128
138
  def add_client
129
- Client.insert(client_number: client_id, client_name: client_name.strip, fund_id: fund_id, provider_id: provider.id, type: provider.type)
139
+ Client.insert(client_number: client_id, client_name: client_name.strip, fund_id: fund_id, provider_id: provider.id, prov_type: provider.prov_type)
130
140
  end
131
141
 
132
142
  # rarely there may be an invoice line that contains an invoice number that already exists. This method accounts for it, by merely updating the amount.
@@ -1,12 +1,15 @@
1
1
  module Sunnyside
2
2
  def self.process_private
3
- Dir["#{LOCAL_FILES}/private/*.PDF", "#{LOCAL_FILES}/private/*.pdf"].select { |file| Filelib.where(filename: file).count == 0 }.each do |file|
4
- puts "processing #{file}..."
5
- PDF::Reader.new(file).pages.each { |inv|
6
- page = inv.text.split(/\n/)
7
- InvoiceParse.new(page).process if page.include?('Remit')
8
- }
3
+ Dir["#{DRIVE}/sunnyside-files/private/*.PDF"].each do |file|
4
+ if Filelib.where(filename: file).count == 0
5
+ puts "processing #{file}..."
6
+ PDF::Reader.new(file).pages.each { |inv|
7
+ page = inv.text.split(/\n/)
8
+ InvoiceParse.new(page).process if page.include?('Remit')
9
+ }
10
+ end
9
11
  Filelib.insert(filename: file, purpose: 'private client visit data')
12
+ FileUtils.mv(file, "#{DRIVE}/sunnyside-files/private/archive/#{File.basename(file)}")
10
13
  end
11
14
  end
12
15
 
@@ -23,7 +26,7 @@ module Sunnyside
23
26
  end
24
27
 
25
28
  def client_number
26
- client_line[/[0-9]{7}/, 0]
29
+ client_line[/[0-9]{7}/]
27
30
  end
28
31
 
29
32
  def process
@@ -2,14 +2,15 @@ module Sunnyside
2
2
  class Menu
3
3
  def start
4
4
  loop do
5
- puts "1.) LEDGER IMPORT"
6
- puts "2.) EDI IMPORT"
7
- puts "3.) 837 IMPORT"
8
- puts "4.) A/R REPORT"
9
- puts "5.) CASH RECEIPT IMPORT"
10
- puts "6.) ACCESS FTP"
11
- puts "7.) EXPIRING AUTHORIZATION REPORT"
12
- puts "9.) MCO - MLTC HOURS UPDATE"
5
+ puts " 1.) LEDGER IMPORT"
6
+ puts " 2.) EDI IMPORT"
7
+ puts " 3.) 837 IMPORT"
8
+ puts " 4.) A/R REPORT"
9
+ puts " 5.) CASH RECEIPT IMPORT"
10
+ puts " 6.) ACCESS FTP"
11
+ puts " 7.) EXPIRING AUTHORIZATION REPORT"
12
+ puts " 9.) MCO - MLTC HOURS UPDATE"
13
+ puts "10.) CUSTOM QUERY"
13
14
  print "select option: "
14
15
  case gets.chomp
15
16
  when '1'
@@ -20,18 +21,19 @@ module Sunnyside
20
21
  when '3'
21
22
  Sunnyside.parse_pdf
22
23
  when '4'
23
- Sunnyside::Report.new
24
+ Sunnyside.run_report
24
25
  when '5'
25
- Sunnyside::CashReceipt.new.process
26
+ Sunnyside.cash_receipt
26
27
  when '6'
27
- Sunnyside.access_ftp(:download)
28
- Sunnyside.access_ftp(:upload)
28
+ Sunnyside.access_ftp
29
29
  when '7'
30
30
  Sunnyside.show_opts
31
31
  when '8'
32
32
  Sunnyside.process_private
33
33
  when '9'
34
34
  Sunnyside.run_mco_mltc
35
+ when '10'
36
+ Sunnyside.query
35
37
  else
36
38
  exit
37
39
  end