twinfieldrb 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/codeql-analysis.yml +70 -0
  3. data/.github/workflows/rspec.yml +33 -0
  4. data/.gitignore +6 -0
  5. data/.rspec +1 -0
  6. data/CHANGELOG.md +15 -0
  7. data/Gemfile +4 -0
  8. data/README.md +120 -0
  9. data/Rakefile +1 -0
  10. data/lib/twinfield/abstract_model.rb +7 -0
  11. data/lib/twinfield/api/base_api.rb +50 -0
  12. data/lib/twinfield/api/finder.rb +45 -0
  13. data/lib/twinfield/api/o_auth_session.rb +58 -0
  14. data/lib/twinfield/api/process.rb +44 -0
  15. data/lib/twinfield/api/session.rb +170 -0
  16. data/lib/twinfield/browse/transaction/cost_center.rb +145 -0
  17. data/lib/twinfield/browse/transaction/customer.rb +413 -0
  18. data/lib/twinfield/browse/transaction/general_ledger.rb +144 -0
  19. data/lib/twinfield/configuration.rb +49 -0
  20. data/lib/twinfield/create/cost_center.rb +39 -0
  21. data/lib/twinfield/create/creditor.rb +88 -0
  22. data/lib/twinfield/create/debtor.rb +88 -0
  23. data/lib/twinfield/create/error.rb +30 -0
  24. data/lib/twinfield/create/general_ledger.rb +39 -0
  25. data/lib/twinfield/create/transaction.rb +97 -0
  26. data/lib/twinfield/customer.rb +612 -0
  27. data/lib/twinfield/helpers/parsers.rb +23 -0
  28. data/lib/twinfield/helpers/transaction_match.rb +40 -0
  29. data/lib/twinfield/request/find.rb +149 -0
  30. data/lib/twinfield/request/list.rb +66 -0
  31. data/lib/twinfield/request/read.rb +111 -0
  32. data/lib/twinfield/sales_invoice.rb +409 -0
  33. data/lib/twinfield/transaction.rb +112 -0
  34. data/lib/twinfield/version.rb +5 -0
  35. data/lib/twinfield.rb +89 -0
  36. data/script/boot.rb +58 -0
  37. data/script/console +2 -0
  38. data/spec/fixtures/cluster/finder/ivt.xml +1 -0
  39. data/spec/fixtures/cluster/processxml/columns/sales_transactions.xml +312 -0
  40. data/spec/fixtures/cluster/processxml/customer/create_success.xml +100 -0
  41. data/spec/fixtures/cluster/processxml/customer/read_success.xml +93 -0
  42. data/spec/fixtures/cluster/processxml/customer/update_success.xml +1 -0
  43. data/spec/fixtures/cluster/processxml/invoice/create_error.xml +8 -0
  44. data/spec/fixtures/cluster/processxml/invoice/create_final_error.xml +67 -0
  45. data/spec/fixtures/cluster/processxml/invoice/create_success.xml +64 -0
  46. data/spec/fixtures/cluster/processxml/invoice/read_not_found.xml +1 -0
  47. data/spec/fixtures/cluster/processxml/invoice/read_success.xml +106 -0
  48. data/spec/fixtures/cluster/processxml/read/deb.xml +12 -0
  49. data/spec/fixtures/cluster/processxml/response.xml +8 -0
  50. data/spec/fixtures/login/session/wsdl.xml +210 -0
  51. data/spec/spec_helper.rb +17 -0
  52. data/spec/stubs/finder_stubs.rb +19 -0
  53. data/spec/stubs/processxml_stubs.rb +41 -0
  54. data/spec/stubs/session_stubs.rb +28 -0
  55. data/spec/twinfield/api/oauth_session_spec.rb +37 -0
  56. data/spec/twinfield/api/process_spec.rb +7 -0
  57. data/spec/twinfield/browse/transaction/cost_center_spec.rb +60 -0
  58. data/spec/twinfield/browse/transaction/general_ledger_spec.rb +60 -0
  59. data/spec/twinfield/browse/transaction/transaction_spec.rb +72 -0
  60. data/spec/twinfield/config_spec.rb +60 -0
  61. data/spec/twinfield/customer_spec.rb +326 -0
  62. data/spec/twinfield/request/find_spec.rb +24 -0
  63. data/spec/twinfield/request/list_spec.rb +58 -0
  64. data/spec/twinfield/request/read_spec.rb +26 -0
  65. data/spec/twinfield/sales_invoice_spec.rb +253 -0
  66. data/spec/twinfield/session_spec.rb +77 -0
  67. data/spec/twinfield/transaction_spec.rb +149 -0
  68. data/twinfieldrb.gemspec +24 -0
  69. data/wsdls/accounting/finder.wsdl +157 -0
  70. data/wsdls/accounting/process.wsdl +199 -0
  71. data/wsdls/accounting/session.wsdl +452 -0
  72. data/wsdls/accounting2/finder.wsdl +157 -0
  73. data/wsdls/accounting2/process.wsdl +199 -0
  74. data/wsdls/api.accounting/finder.wsdl +157 -0
  75. data/wsdls/api.accounting/process.wsdl +199 -0
  76. data/wsdls/session.wsdl +210 -0
  77. data/wsdls/update +10 -0
  78. metadata +196 -0
@@ -0,0 +1,19 @@
1
+ module FinderStubs
2
+ def stub_finder type, oauth: false
3
+ stub_request(:post, "https://accounting.twinfield.com/webservices/finder.asmx")
4
+ .with(
5
+ body: /<Search xmlns="http:\/\/www.twinfield.com\/"><type>#{type}<\/type><pattern>\*<\/pattern><field>0<\/field><firstRow>1<\/firstRow><maxRows>100<\/maxRows><options><\/options><\/Search>/,
6
+ headers: {
7
+ "Soapaction" => '"http://www.twinfield.com/Search"'
8
+ }
9
+ )
10
+ .to_return(status: 200, body: File.read(File.expand_path("../../fixtures/cluster/finder/#{type.downcase}.xml", __FILE__)), headers: {})
11
+ end
12
+
13
+ private
14
+
15
+ def twinfield_header(oauth, company = nil)
16
+ company_fragment = company ? "<CompanyCode>#{company}</CompanyCode>" : ""
17
+ oauth ? "<Header xmlns=\"http://www.twinfield.com/\"><AccessToken>2b128baa05dd3cabc61e534435884961</AccessToken>#{company_fragment}</Header>" : "<Header xmlns=\"http://www.twinfield.com/\"><SessionID>session_id</SessionID></Header>"
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ module ProcessxmlStubs
2
+ def stub_processxml_list_dimensions dimension_type: "DEB", company: "company", oauth: false
3
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
4
+ .with(
5
+ body: /<soap:Body><ProcessXmlString xmlns="http:\/\/www.twinfield.com\/"><xmlRequest><!\[CDATA\[\n <list>\n <type>dimensions<\/type>\n <dimtype>#{dimension_type}<\/dimtype>\n<office>#{company}<\/office>\n <\/list>\n \]\]><\/xmlRequest><\/ProcessXmlString><\/soap:Body>/,
6
+ headers: {
7
+ "Soapaction" => '"http://www.twinfield.com/ProcessXmlString"'
8
+ }
9
+ )
10
+ .to_return(status: 200, body: File.read(File.expand_path("../../fixtures/cluster/processxml/response.xml", __FILE__)), headers: {})
11
+ end
12
+
13
+ def stub_processxml_list_offices oauth: false, company: nil
14
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
15
+ .with(
16
+ body: /<soap:Body><ProcessXmlString xmlns="http:\/\/www.twinfield.com\/"><xmlRequest><!\[CDATA\[\n <list>\n <type>offices<\/type>\n \n <\/list>\n \]\]><\/xmlRequest><\/ProcessXmlString><\/soap:Body>/,
17
+ headers: {
18
+ "Soapaction" => '"http://www.twinfield.com/ProcessXmlString"'
19
+ }
20
+ )
21
+ .to_return(status: 200, body: File.read(File.expand_path("../../fixtures/cluster/processxml/response.xml", __FILE__)), headers: {})
22
+ end
23
+
24
+ def stub_processxml_read_dimensions dimtype, oauth: false, company: nil
25
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
26
+ .with(
27
+ body: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><soap:Envelope xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://www.twinfield.com/\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Header>#{twinfield_header(oauth, company)}</soap:Header><soap:Body><ProcessXmlString xmlns=\"http://www.twinfield.com/\"><xmlRequest><![CDATA[\n <read>\n <type>dimensions</type>\n <dimtype>#{dimtype}</dimtype>\n </read>\n ]]></xmlRequest></ProcessXmlString></soap:Body></soap:Envelope>",
28
+ headers: {
29
+ "Soapaction" => '"http://www.twinfield.com/ProcessXmlString"'
30
+ }
31
+ )
32
+ .to_return(status: 200, body: File.read(File.expand_path("../../fixtures/cluster/processxml/read/#{dimtype.downcase}.xml", __FILE__)), headers: {})
33
+ end
34
+
35
+ private
36
+
37
+ def twinfield_header(oauth, company = nil)
38
+ company_fragment = company ? "<CompanyCode>#{company}</CompanyCode>" : ""
39
+ oauth ? "<Header xmlns=\"http://www.twinfield.com/\"><AccessToken>2b128baa05dd3cabc61e534435884961</AccessToken>#{company_fragment}</Header>" : "<Header xmlns=\"http://www.twinfield.com/\"><SessionID>session_id</SessionID></Header>"
40
+ end
41
+ end
@@ -0,0 +1,28 @@
1
+ module SessionStubs
2
+ def stub_create_session username: "username", password: "password", organisation: "organisation", response: "Ok"
3
+ stub_request(:post, "https://login.twinfield.com/webservices/session.asmx")
4
+ .with(
5
+ body: /<tns:user>#{username}<\/tns:user><tns:password>#{password}<\/tns:password><tns:organisation>#{organisation}<\/tns:organisation><\/tns:Logon>/,
6
+ headers: {
7
+ Soapaction: '"http://www.twinfield.com/Logon"'
8
+
9
+ }
10
+ ).to_return(status: 200, body: "<env:Envelope><env:Header><Header><SessionId>session_id</SessionId></Header></env:Header><env:Body><LogonResponse><LogonResult>#{response}</LogonResult><Cluster>https://accounting.twinfield.com</Cluster></LogonResponse></env:Body></env:Envelope>")
11
+ end
12
+
13
+ def stub_cluster_session_wsdl
14
+ stub_request(:get, "https://accounting.twinfield.com/webservices/session.asmx?wsdl")
15
+ .to_return(status: 200, body: File.read(File.expand_path("../../../wsdls/accounting/session.wsdl", __FILE__)))
16
+ end
17
+
18
+ def stub_select_company company: "company"
19
+ stub_request(:post, "https://accounting.twinfield.com/webservices/session.asmx")
20
+ .with(
21
+ body: /<soap:Body><SelectCompany xmlns="http:\/\/www.twinfield.com\/"><company>#{company}<\/company><\/SelectCompany><\/soap:Body>/,
22
+ headers: {
23
+ "Soapaction" => '"http://www.twinfield.com/SelectCompany"'
24
+ }
25
+ )
26
+ .to_return(status: 200, body: "", headers: {})
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe Twinfield::Api::OAuthSession do
4
+ include SessionStubs
5
+
6
+ context "OAuth configured" do
7
+ before do
8
+ Twinfield.configure do |config|
9
+ config.session_type = "Twinfield::Api::OAuthSession"
10
+ config.cluster = "https://accounting.twinfield.com"
11
+ config.access_token = "2b128baa05dd3cabc61e534435884961"
12
+ end
13
+ end
14
+
15
+ after do
16
+ Twinfield.configure do |config|
17
+ config.session_type = nil
18
+ config.cluster = nil
19
+ config.access_token = nil
20
+ end
21
+ end
22
+
23
+ it "returns true when oauth is configured" do
24
+ session = Twinfield::Api::OAuthSession.new
25
+
26
+ expect(session.connected?).to be_truthy
27
+ end
28
+ end
29
+
30
+ describe "#connected?" do
31
+ it "returns false when no access token is supplied" do
32
+ session = Twinfield::Api::OAuthSession.new
33
+
34
+ expect(session.connected?).to be_falsey
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,7 @@
1
+ require "spec_helper"
2
+
3
+ describe Twinfield::Api::Process do
4
+ it "...." do
5
+ # TODO
6
+ end
7
+ end
@@ -0,0 +1,60 @@
1
+ require "spec_helper"
2
+
3
+ describe Twinfield::Browse::Transaction::CostCenter do
4
+ include SessionStubs
5
+ include FinderStubs
6
+ include ProcessxmlStubs
7
+
8
+ before do
9
+ stub_create_session
10
+ stub_cluster_session_wsdl
11
+ stub_select_company
12
+ end
13
+
14
+ describe "class methods" do
15
+ describe ".where" do
16
+ it "returns transactions" do
17
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
18
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
19
+
20
+ transaction = Twinfield::Browse::Transaction::CostCenter.where.first
21
+
22
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::CostCenter)
23
+ expect(transaction.value).to eql(2200.0)
24
+ end
25
+
26
+ it "accepts a customer code" do
27
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
28
+ .with(body: /<from>1000\/01<\/from>/)
29
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
30
+
31
+ transaction = Twinfield::Browse::Transaction::CostCenter.where(years: 1000..2000).first
32
+
33
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::CostCenter)
34
+ expect(transaction.value).to eql(2200.0)
35
+ end
36
+
37
+ it "accepts a customer code" do
38
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
39
+ .with(body: /<from>4040<\/from>\s*<to>4040<\/to>/)
40
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
41
+
42
+ transaction = Twinfield::Browse::Transaction::CostCenter.where(dim1: "4040").first
43
+
44
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::CostCenter)
45
+ expect(transaction.value).to eql(2200.0)
46
+ end
47
+
48
+ it "accepts a invoice number" do
49
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
50
+ .with(body: /abcd12002/)
51
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
52
+
53
+ transaction = Twinfield::Browse::Transaction::CostCenter.where(dim2: "abcd12002").first
54
+
55
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::CostCenter)
56
+ expect(transaction.value).to eql(2200.0)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,60 @@
1
+ require "spec_helper"
2
+
3
+ describe Twinfield::Browse::Transaction::GeneralLedger do
4
+ include SessionStubs
5
+ include FinderStubs
6
+ include ProcessxmlStubs
7
+
8
+ before do
9
+ stub_create_session
10
+ stub_cluster_session_wsdl
11
+ stub_select_company
12
+ end
13
+
14
+ describe "class methods" do
15
+ describe ".where" do
16
+ it "returns transactions" do
17
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
18
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
19
+
20
+ transaction = Twinfield::Browse::Transaction::GeneralLedger.where.first
21
+
22
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::GeneralLedger)
23
+ expect(transaction.value).to eql(2200.0)
24
+ end
25
+
26
+ it "accepts a customer code" do
27
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
28
+ .with(body: /<from>1000\/01<\/from>/)
29
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
30
+
31
+ transaction = Twinfield::Browse::Transaction::GeneralLedger.where(years: 1000..2000).first
32
+
33
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::GeneralLedger)
34
+ expect(transaction.value).to eql(2200.0)
35
+ end
36
+
37
+ it "accepts a customer code" do
38
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
39
+ .with(body: /<from>4040<\/from>\s*<to>4040<\/to>/)
40
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
41
+
42
+ transaction = Twinfield::Browse::Transaction::GeneralLedger.where(dim1: "4040").first
43
+
44
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::GeneralLedger)
45
+ expect(transaction.value).to eql(2200.0)
46
+ end
47
+
48
+ it "accepts a invoice number" do
49
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
50
+ .with(body: /abcd12002/)
51
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
52
+
53
+ transaction = Twinfield::Browse::Transaction::GeneralLedger.where(dim2: "abcd12002").first
54
+
55
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::GeneralLedger)
56
+ expect(transaction.value).to eql(2200.0)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,72 @@
1
+ require "spec_helper"
2
+
3
+ describe Twinfield::Browse::Transaction::Customer do
4
+ include SessionStubs
5
+ include FinderStubs
6
+ include ProcessxmlStubs
7
+
8
+ before do
9
+ stub_create_session
10
+ stub_cluster_session_wsdl
11
+ stub_select_company
12
+ end
13
+
14
+ describe "instance methods" do
15
+ describe "#to_transaction_match_line_xml" do
16
+ it "returns a valid piece of xml" do
17
+ xml = Nokogiri::XML(Twinfield::Browse::Transaction::Customer.new(code: "VRK", number: "20210120").to_transaction_match_line_xml(1))
18
+ xml.css("line transcode").text
19
+ xml.css("line transnumber").text
20
+ xml.css("line transline").text == "1"
21
+ end
22
+ end
23
+ end
24
+
25
+ describe "class methods" do
26
+ describe ".where" do
27
+ it "returns transactions" do
28
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
29
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
30
+
31
+ transaction = Twinfield::Browse::Transaction::Customer.where.first
32
+
33
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::Customer)
34
+ expect(transaction.value).to eql(2200.0)
35
+ end
36
+
37
+ it "accepts a customer code" do
38
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
39
+ .with(body: /abcd12002/)
40
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
41
+
42
+ transaction = Twinfield::Browse::Transaction::Customer.where(customer_code: "abcd12002").first
43
+
44
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::Customer)
45
+ expect(transaction.value).to eql(2200.0)
46
+ expect(transaction.date).to eql(Date.new(2021, 12, 5))
47
+ end
48
+
49
+ it "accepts a customer code" do
50
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
51
+ .with(body: /abcd12002/)
52
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
53
+
54
+ transaction = Twinfield::Browse::Transaction::Customer.where(customer_code: "abcd12002").first
55
+
56
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::Customer)
57
+ expect(transaction.value).to eql(2200.0)
58
+ end
59
+
60
+ it "accepts a invoice number" do
61
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
62
+ .with(body: /abcd12002/)
63
+ .to_return(body: File.read(File.expand_path("../../../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
64
+
65
+ transaction = Twinfield::Browse::Transaction::Customer.where(invoice_number: "abcd12002").first
66
+
67
+ expect(transaction).to be_a(Twinfield::Browse::Transaction::Customer)
68
+ expect(transaction.value).to eql(2200.0)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,60 @@
1
+ require "spec_helper"
2
+
3
+ describe Twinfield::Configuration do
4
+ after do
5
+ reset_config
6
+ end
7
+
8
+ it "configures username" do
9
+ Twinfield.configure do |config|
10
+ config.username = "my_username"
11
+ end
12
+ expect(Twinfield.configuration.username).to eq "my_username"
13
+ end
14
+
15
+ it "configures password" do
16
+ Twinfield.configure do |config|
17
+ config.password = "my_password"
18
+ end
19
+ expect(Twinfield.configuration.password).to eq "my_password"
20
+ end
21
+
22
+ it "configures organisation" do
23
+ Twinfield.configure do |config|
24
+ config.organisation = "my_organisation"
25
+ end
26
+ expect(Twinfield.configuration.organisation).to eq "my_organisation"
27
+ end
28
+
29
+ describe "#session_class" do
30
+ it "returns a session class" do
31
+ expect(Twinfield.configuration.session_class).to eq(Twinfield::Api::Session)
32
+ end
33
+
34
+ it "returns a oauth session class when configured" do
35
+ Twinfield.configure do |config|
36
+ config.session_type = "Twinfield::Api::OAuthSession"
37
+ end
38
+ expect(Twinfield.configuration.session_class).to eq(Twinfield::Api::OAuthSession)
39
+ Twinfield.configure do |config|
40
+ config.session_type = nil
41
+ end
42
+ end
43
+ end
44
+
45
+ describe "#access_token_expired?" do
46
+ it "raises when no access token is given" do
47
+ expect { Twinfield.configuration.access_token_expired? }.to raise_error(Twinfield::Configuration::Error)
48
+ end
49
+
50
+ it "returns expired on an old access token" do
51
+ Twinfield.configuration.access_token = "abc.#{Base64.encode64(JSON({exp: (Time.now - 100).to_i}))}.abc"
52
+ expect(Twinfield.configuration.access_token_expired?).to eq(true)
53
+ end
54
+
55
+ it "returns not expired on an old access token" do
56
+ Twinfield.configuration.access_token = "abc.#{Base64.encode64(JSON({exp: (Time.now + 100).to_i}))}.abc"
57
+ expect(Twinfield.configuration.access_token_expired?).to eq(false)
58
+ end
59
+ end
60
+ end