twinfieldrb 0.3.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.
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,253 @@
1
+ require "spec_helper"
2
+
3
+ describe Twinfield::SalesInvoice do
4
+ include SessionStubs
5
+ include ProcessxmlStubs
6
+
7
+ describe Twinfield::SalesInvoice::Line do
8
+ describe "class methods" do
9
+ describe ".new" do
10
+ it "should keep decials intact" do
11
+ expect(Twinfield::SalesInvoice::Line.new(quantity: 1.5, article: "A").quantity).to match(1.5)
12
+ end
13
+ end
14
+ end
15
+
16
+ describe "#to_xml" do
17
+ it "renders xml" do
18
+ expect(Twinfield::SalesInvoice::Line.new(article: "A").to_xml(1)).to match("<line id=\"1\">")
19
+ expect(Twinfield::SalesInvoice::Line.new(article: "A", id: 2).to_xml).to match("<line id=\"2\">")
20
+ end
21
+ it "formats dates correctly" do
22
+ expect(Twinfield::SalesInvoice::Line.new(article: "A", performancedate: Date.new(2020, 12, 23)).to_xml(1)).to match("20201223")
23
+ end
24
+ end
25
+
26
+ describe "#invoice" do
27
+ it "returns the containers invoice" do
28
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP")
29
+ invoice.lines = [Twinfield::SalesInvoice::Line.new(article: "A")]
30
+ expect(invoice.associated_lines.first.invoice).to eq(invoice)
31
+ end
32
+ end
33
+ end
34
+
35
+ describe "class methods" do
36
+ before do
37
+ stub_create_session
38
+ stub_cluster_session_wsdl
39
+ stub_select_company
40
+ end
41
+
42
+ describe ".find" do
43
+ it "returns a sales invoice" do
44
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
45
+ .with(body: /<read>\s*<type>salesinvoice<\/type>/)
46
+ .to_return(body: File.read(File.expand_path("../../fixtures/cluster/processxml/invoice/read_success.xml", __FILE__)))
47
+ invoice = Twinfield::SalesInvoice.find(13, invoicetype: "VERKOOP")
48
+ expect(invoice).to be_a(Twinfield::SalesInvoice)
49
+ expect(invoice.invoicenumber).to eq("13")
50
+ expect(invoice.financials.number).to eq("202100006")
51
+ expect(invoice.lines.first).to be_a(Twinfield::SalesInvoice::Line)
52
+ expect(invoice.lines[2].description).to eq("Custom article")
53
+ expect(invoice.vat_lines[0].vatname).to eq("BTW 21%")
54
+ end
55
+
56
+ it "returns nil when empty result" do
57
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
58
+ .with(body: /<read>\s*<type>salesinvoice<\/type>/)
59
+ .to_return(body: File.read(File.expand_path("../../fixtures/cluster/processxml/invoice/read_not_found.xml", __FILE__)))
60
+ invoice = Twinfield::SalesInvoice.find(13, invoicetype: "VERKOOP")
61
+ expect(invoice).to eq(nil)
62
+ end
63
+ end
64
+
65
+ describe ".new" do
66
+ it "initializes from a hash" do
67
+ hash = {lines: [{id: "1", article: "HUUR", subarticle: "1", quantity: 1, units: 1, allowdiscountorpremium: "true", description: "Huur", unitspriceexcl: 7.23, unitspriceinc: nil, valueinc: 8.75, freetext1: nil, freetext2: nil, freetext3: nil, dim1: "8000", vatcode: "VH", performancetype: nil, performancedate: nil}, {id: "2", article: "-", subarticle: nil, quantity: nil, units: nil, allowdiscountorpremium: nil, description: "Beschrijving", unitspriceexcl: nil, unitspriceinc: nil, freetext1: nil, freetext2: nil, freetext3: nil, dim1: nil, vatcode: nil, performancetype: nil, performancedate: nil}], vat_lines: [{vatcode: "VH", vatvalue: 1.73, performancetype: "", performancedate: "", vatname: "BTW 21%"}], invoicetype: "FACTUUR", invoicedate: "2022-06-24", duedate: "2022-07-24", performancedate: nil, bank: "BNK", invoiceaddressnumber: 1, deliveraddressnumber: 1, customer_code: 2321, period: "2022/6", currency: "EUR", status: "final", paymentmethod: "bank", headertext: "", footertext: "Footer", office: "NL123", invoicenumber: "1622"}
68
+ invoice = Twinfield::SalesInvoice.new(**hash)
69
+ expect(invoice.invoicedate).to eq(Date.new(2022, 6, 24))
70
+ expect(invoice.invoicedate).not_to eq("2022-06-24")
71
+ end
72
+ end
73
+ end
74
+
75
+ describe "instance methods" do
76
+ describe "#final?" do
77
+ it "returns false by default" do
78
+ invoice = Twinfield::SalesInvoice.new(invoicetype: "F", customer_code: 12)
79
+ expect(invoice.final?).to be_falsey
80
+ end
81
+
82
+ it "returns true when status == final" do
83
+ invoice = Twinfield::SalesInvoice.new(invoicetype: "F", customer_code: 12, status: :final)
84
+ expect(invoice.final?).to be_truthy
85
+ end
86
+ end
87
+
88
+ describe "#transaction" do
89
+ it "retuns no transaction when no financials info" do
90
+ stub_create_session
91
+ stub_cluster_session_wsdl
92
+ stub_select_company
93
+
94
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP", invoicenumber: "2021-0812")
95
+ invoice.transaction
96
+ expect(invoice.transaction).to be_nil
97
+ end
98
+
99
+ it "returns a transaction" do
100
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP", invoicenumber: "2021-0812")
101
+ invoice.financials = Twinfield::SalesInvoice::Financials.new(code: "VRK", number: "20210812")
102
+
103
+ expect(Twinfield::Browse::Transaction::Customer).to receive(:find).with(code: "VRK", number: "20210812")
104
+ invoice.transaction
105
+ end
106
+
107
+ it "returns a transaction" do
108
+ stub_create_session
109
+ stub_cluster_session_wsdl
110
+ stub_select_company
111
+
112
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
113
+ .with(body: /20210812/)
114
+ .to_return(body: File.read(File.expand_path("../../fixtures/cluster/processxml/columns/sales_transactions.xml", __FILE__)))
115
+
116
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP", invoicenumber: "2021-0812")
117
+ invoice.financials = Twinfield::SalesInvoice::Financials.new(code: "VRK", number: "20210812")
118
+
119
+ invoice.transaction
120
+ expect(invoice.transaction).to be_a(Twinfield::Browse::Transaction::Customer)
121
+ end
122
+ end
123
+ describe "#to_xml" do
124
+ it "renders xml" do
125
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP")
126
+ invoice.lines = [Twinfield::SalesInvoice::Line.new(article: "A")]
127
+ expect(invoice.to_xml).to match("<lines>\n <line id=\"1\">")
128
+ expect(invoice.to_xml).not_to match("<invoicenumber")
129
+ end
130
+
131
+ it "renders xml with invoicenumber when given (updating)" do
132
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP", invoicenumber: "12")
133
+ invoice.lines = [Twinfield::SalesInvoice::Line.new(article: "A")]
134
+ expect(invoice.to_xml).to match("<invoicenumber>12</invoicenumber>")
135
+ end
136
+ end
137
+
138
+ describe "#to_h" do
139
+ let(:invoice) {
140
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP")
141
+ invoice.lines = [Twinfield::SalesInvoice::Line.new(article: "A")]
142
+ invoice
143
+ }
144
+ it "renders a hash" do
145
+ expect(invoice.to_h).to be_a Hash
146
+ expect(invoice.to_h[:customer_code]).to eq(1001)
147
+ expect(invoice.to_h[:lines][0][:article]).to eq("A")
148
+ end
149
+
150
+ it "enables a roundtrip" do
151
+ invoice2 = Twinfield::SalesInvoice.new(**invoice.to_h)
152
+ expect(invoice2.duedate).to eq(invoice.duedate)
153
+ expect(invoice2.lines.first.article).to eq("A")
154
+ end
155
+ end
156
+
157
+ describe ":financials" do
158
+ before do
159
+ stub_create_session
160
+ stub_cluster_session_wsdl
161
+ stub_select_company
162
+ end
163
+
164
+ it "survives a roundtrip" do
165
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
166
+ .with(body: /<read>\s*<type>salesinvoice<\/type>/)
167
+ .to_return(body: File.read(File.expand_path("../../fixtures/cluster/processxml/invoice/read_success.xml", __FILE__)))
168
+ invoice = Twinfield::SalesInvoice.find(13, invoicetype: "VERKOOP")
169
+ expect(invoice.financials).to be_a Twinfield::SalesInvoice::Financials
170
+ financials_number = "202100006"
171
+
172
+ expect(invoice.financials.number).to eq financials_number
173
+ expect(invoice.to_h[:financials][:number]).to eq financials_number
174
+
175
+ Twinfield::SalesInvoice.new(**invoice.to_h)
176
+ expect(invoice.financials.number).to eq financials_number
177
+ end
178
+ end
179
+
180
+ describe "#save" do
181
+ before do
182
+ stub_create_session
183
+ stub_cluster_session_wsdl
184
+ stub_select_company
185
+ end
186
+
187
+ it "reports errors" do
188
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
189
+ .with(body: /<customer>1001<\/customer>/)
190
+ .to_return(body: File.read(File.expand_path("../../fixtures/cluster/processxml/invoice/create_error.xml", __FILE__)))
191
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP")
192
+ expect { invoice.save }.to raise_error(Twinfield::Create::Error)
193
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP")
194
+ expect { invoice.save }.to raise_error(Twinfield::Create::EmptyInvoice)
195
+ end
196
+
197
+ it "reports errors when fixed because finalized" do
198
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
199
+ .with(body: /<customer>1001<\/customer>/)
200
+ .to_return(body: File.read(File.expand_path("../../fixtures/cluster/processxml/invoice/create_final_error.xml", __FILE__)))
201
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP")
202
+ expect { invoice.save }.to raise_error(Twinfield::Create::Error)
203
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP")
204
+ expect { invoice.save }.to raise_error(Twinfield::Create::Finalized)
205
+ end
206
+
207
+ it "succeeds" do
208
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
209
+ .with(body: /<customer>1001<\/customer>/)
210
+ .to_return(body: File.read(File.expand_path("../../fixtures/cluster/processxml/invoice/create_success.xml", __FILE__)))
211
+ invoice = Twinfield::SalesInvoice.new(duedate: Time.now, customer: "1001", invoicetype: "VERKOOP")
212
+ saved_invoice = invoice.save
213
+ expect(saved_invoice.invoicenumber).to eq("3")
214
+ end
215
+ end
216
+
217
+ describe "totals" do
218
+ before do
219
+ stub_create_session
220
+ stub_cluster_session_wsdl
221
+ stub_select_company
222
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
223
+ .with(body: /<read>\s*<type>salesinvoice<\/type>/)
224
+ .to_return(body: File.read(File.expand_path("../../fixtures/cluster/processxml/invoice/read_success.xml", __FILE__)))
225
+ @invoice = Twinfield::SalesInvoice.find(13, invoicetype: "VERKOOP")
226
+ end
227
+
228
+ describe "#total" do
229
+ it "0 on an empty invoice" do
230
+ expect(Twinfield::SalesInvoice.new(invoicetype: "VERKOOP", customer: "1001").total).to eq(0)
231
+ end
232
+
233
+ it "sums lines perfectly" do
234
+ expect(@invoice.total).to eq(221.0)
235
+ end
236
+
237
+ it "defaults to total incl vat" do
238
+ expect(@invoice.totalinc).to eq(@invoice.total)
239
+ end
240
+ end
241
+
242
+ describe "#totalexcl" do
243
+ it "0 on an empty invoice" do
244
+ expect(Twinfield::SalesInvoice.new(invoicetype: "VERKOOP", customer: "1001").total).to eq(0)
245
+ end
246
+
247
+ it "sums lines perfectly" do
248
+ expect(@invoice.totalexcl).to eq(200.0)
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,77 @@
1
+ require "spec_helper"
2
+
3
+ describe Twinfield::Api::Session do
4
+ include SessionStubs
5
+
6
+ after do
7
+ reset_config
8
+ end
9
+
10
+ describe "successful logon" do
11
+ before(:all) do
12
+ stub_create_session
13
+ stub_cluster_session_wsdl
14
+ stub_select_company
15
+ @session = Twinfield::Api::Session.new
16
+ @session.logon
17
+ end
18
+
19
+ it "should return successful message" do
20
+ expect(@session.status).to eq "Ok"
21
+ end
22
+
23
+ it "should return that the current session already is connected" do
24
+ expect(@session.logon).to eq "already connected"
25
+ end
26
+
27
+ it "should return that the current session already is connected" do
28
+ stub_create_session
29
+ stub_cluster_session_wsdl
30
+ stub_select_company
31
+ expect(@session.relog).to eq "Ok"
32
+ end
33
+
34
+ it "should have a session_id after successful logon" do
35
+ expect(@session.session_id).not_to eq nil
36
+ end
37
+
38
+ it "should have a cluster after successful logon" do
39
+ expect(@session.cluster).not_to eq nil
40
+ end
41
+
42
+ it "should return true for connected" do
43
+ expect(@session.connected?).to eq true
44
+ end
45
+ end
46
+
47
+ describe "invalid logon" do
48
+ before(:all) do
49
+ username = "not_valid_username"
50
+
51
+ stub_create_session username: username, response: "Invalid"
52
+
53
+ Twinfield.configure do |config|
54
+ config.username = username
55
+ end
56
+
57
+ @session = Twinfield::Api::Session.new
58
+ @session.logon
59
+ end
60
+
61
+ it "should return invalid message" do
62
+ expect(@session.status).to eq "Invalid"
63
+ end
64
+
65
+ it "should not have a session_id" do
66
+ expect(@session.session_id).to eq nil
67
+ end
68
+
69
+ it "should not have a cluster" do
70
+ expect(@session.cluster).to eq nil
71
+ end
72
+
73
+ it "should return false for connected" do
74
+ expect(@session.connected?).to eq false
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,149 @@
1
+ require "spec_helper"
2
+
3
+ describe Twinfield::Transaction do
4
+ include SessionStubs
5
+ include ProcessxmlStubs
6
+
7
+ let(:today_string) do
8
+ Date.today.strftime("%Y%m%d")
9
+ end
10
+ let(:payment_transaction) do
11
+ trans = Twinfield::Transaction.new(code: "PIN", currency: "EUR", date: Date.new(2021, 1, 1))
12
+ trans.lines << Twinfield::Transaction::Line.new(type: :detail, balance_code: 1300, value: 60.5, debitcredit: :credit, customer_code: 1003, invoicenumber: 14)
13
+ trans.lines << Twinfield::Transaction::Line.new(type: :detail, balance_code: 1234, value: 60.5, debitcredit: :debit)
14
+ trans.lines << Twinfield::Transaction::Line.new(type: :total, balance_code: "1230", value: 0.0, debitcredit: :debit)
15
+ trans
16
+ end
17
+
18
+ describe Twinfield::Transaction::Line do
19
+ describe "#to_xml" do
20
+ it "converts a detail line to xml" do
21
+ expect(Twinfield::Transaction::Line.new(type: :detail, balance_code: 1055, value: 60.5, debitcredit: :debit).to_xml).to eq("<line type=\"detail\">\n <dim1>1055</dim1>\n <value>60.5</value>\n <debitcredit>debit</debitcredit>\n <currencydate>#{today_string}</currencydate>\n</line>")
22
+ expect(Twinfield::Transaction::Line.new(type: :detail, balance_code: 8020, value: 50, debitcredit: :credit, vatcode: "VH").to_xml).to eq("<line type=\"detail\">\n <dim1>8020</dim1>\n <value>50.0</value>\n <debitcredit>credit</debitcredit>\n <vatcode>VH</vatcode>\n <currencydate>#{today_string}</currencydate>\n</line>")
23
+ end
24
+ it "converts a total line to xml" do
25
+ expect(Twinfield::Transaction::Line.new(type: :total, balance_code: "0000", value: 0.0, debitcredit: :debit).to_xml).to eq("<line type=\"total\">\n <dim1>0000</dim1>\n <value>0.0</value>\n <debitcredit>debit</debitcredit>\n <currencydate>#{today_string}</currencydate>\n</line>")
26
+ end
27
+ end
28
+ end
29
+
30
+ describe "#value" do
31
+ it "returns the negative value" do
32
+ expect(payment_transaction.value).to eq(-60.5)
33
+ end
34
+ end
35
+
36
+ describe "#to_xml" do
37
+ let(:expected_xml) {
38
+ "<transaction destiny=\"final\">
39
+ <header>
40
+ <office>company</office>
41
+ <code>PIN</code>
42
+ <currency>EUR</currency>
43
+ <date>20210101</date>
44
+ <period>2021/01</period>
45
+ </header>
46
+ <lines>
47
+ <line type=\"total\">
48
+ <dim1>1230</dim1>
49
+ <value>0.0</value>
50
+ <debitcredit>debit</debitcredit>
51
+ <currencydate>#{today_string}</currencydate>
52
+ </line>
53
+ <line type=\"detail\">
54
+ <dim1>1300</dim1>
55
+ <dim2>1003</dim2>
56
+ <value>60.5</value>
57
+ <debitcredit>credit</debitcredit>
58
+ <invoicenumber>14</invoicenumber>
59
+ <currencydate>#{today_string}</currencydate>
60
+ </line>
61
+ <line type=\"detail\">
62
+ <dim1>1234</dim1>
63
+ <value>60.5</value>
64
+ <debitcredit>debit</debitcredit>
65
+ <currencydate>#{today_string}</currencydate>
66
+ </line>
67
+ </lines>
68
+ </transaction>"
69
+ }
70
+
71
+ it "converts a PIN transaction to xml" do
72
+ expect(payment_transaction.to_xml).to eq(expected_xml)
73
+ end
74
+
75
+ context "with hash" do
76
+ let(:payment_transaction) do
77
+ trans_h = {
78
+ code: "PIN", currency: "EUR", date: Date.new(2021, 1, 1), lines: [
79
+ {type: :total, balance_code: "1230", value: 0.0, debitcredit: :debit},
80
+ {type: :detail, balance_code: 1300, value: 60.5, debitcredit: :credit, customer_code: 1003, invoicenumber: 14},
81
+ {type: :detail, balance_code: 1234, value: 60.5, debitcredit: :debit}
82
+ ]
83
+ }
84
+ Twinfield::Transaction.new(**trans_h)
85
+ end
86
+
87
+ it "converts a PIN transaction to xml" do
88
+ expect(payment_transaction.to_xml).to eq(expected_xml)
89
+ end
90
+ end
91
+ end
92
+
93
+ describe "#to_transaction_match_line_xml" do
94
+ it "returns a valid piece of xml" do
95
+ xml = Nokogiri::XML(Twinfield::Transaction.new(code: "PIN", number: "20210121").to_transaction_match_line_xml(2))
96
+ xml.css("line transcode").text
97
+ xml.css("line transnumber").text
98
+ xml.css("line transline").text == "1"
99
+ end
100
+ end
101
+
102
+ describe "#to_match_xml" do
103
+ it "doesn't work if values don't match" do
104
+ expect { payment_transaction.to_match_xml(Twinfield::Browse::Transaction::Customer.new(value: 20, code: "VRK")) }.to raise_error(Twinfield::TransactionMatchError)
105
+ end
106
+
107
+ it "returns xml if values match" do
108
+ matchdatestring = Date.today.strftime("%Y%m%d")
109
+ expect(payment_transaction.to_match_xml(Twinfield::Browse::Transaction::Customer.new(value: 60.5, code: "VRK"))).to eq("<match>
110
+ <set>
111
+ <matchcode>170</matchcode>
112
+ <office>company</office>
113
+ <matchdate>#{matchdatestring}</matchdate>
114
+ <lines>
115
+ <line><transcode>PIN</transcode><transnumber></transnumber><transline>1</transline></line>
116
+ <line><transcode>VRK</transcode><transnumber></transnumber><transline>2</transline></line>
117
+ </lines>
118
+ </set>
119
+ </match>")
120
+ end
121
+ end
122
+
123
+ describe "#match!" do
124
+ let(:error_body) { '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ProcessXmlStringResponse xmlns="http://www.twinfield.com/"><ProcessXmlStringResult>&lt;match result="0"&gt;&lt;set result="0"&gt;&lt;matchcode name="Debiteuren afletteren"&gt;170&lt;/matchcode&gt;&lt;office&gt;NLA002058&lt;/office&gt;&lt;matchdate&gt;20220203&lt;/matchdate&gt;&lt;lines result="0"&gt;&lt;line msgtype="error" msg="Boeking PIN 202200003 regel 1 is niet beschikbaar voor afletteren." result="0"&gt;&lt;transcode&gt;PIN&lt;/transcode&gt;&lt;transnumber&gt;202200003&lt;/transnumber&gt;&lt;transline&gt;1&lt;/transline&gt;&lt;origin&gt;import&lt;/origin&gt;&lt;status&gt;final&lt;/status&gt;&lt;/line&gt;&lt;line msgtype="error" msg="Boeking VRK 202100008 regel 2 is niet beschikbaar voor afletteren." result="0"&gt;&lt;transcode&gt;VRK&lt;/transcode&gt;&lt;transnumber&gt;202100008&lt;/transnumber&gt;&lt;transline&gt;2&lt;/transline&gt;&lt;origin&gt;invoice&lt;/origin&gt;&lt;status&gt;final&lt;/status&gt;&lt;/line&gt;&lt;/lines&gt;&lt;/set&gt;&lt;/match&gt;</ProcessXmlStringResult></ProcessXmlStringResponse></soap:Body></soap:Envelope>' }
125
+ let(:success_body) { '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ProcessXmlStringResponse xmlns="http://www.twinfield.com/"><ProcessXmlStringResult>&lt;match result="1"&gt;&lt;set result="1"&gt;&lt;matchcode name="Debiteuren afletteren"&gt;170&lt;/matchcode&gt;&lt;office&gt;NLA002058&lt;/office&gt;&lt;matchdate&gt;20220203&lt;/matchdate&gt;&lt;lines&gt;&lt;line result="1"&gt;&lt;transcode name="Verkoopfactuur" shortname="Verkoop"&gt;VRK&lt;/transcode&gt;&lt;transnumber&gt;202100008&lt;/transnumber&gt;&lt;transline&gt;1&lt;/transline&gt;&lt;origin&gt;invoice&lt;/origin&gt;&lt;status&gt;final&lt;/status&gt;&lt;dim1&gt;1300&lt;/dim1&gt;&lt;dim2&gt;1003&lt;/dim2&gt;&lt;matchlevel&gt;2&lt;/matchlevel&gt;&lt;basevalueopen&gt;221.00&lt;/basevalueopen&gt;&lt;repvalueopen&gt;221.00&lt;/repvalueopen&gt;&lt;valueopen&gt;221.00&lt;/valueopen&gt;&lt;matchvalue&gt;221.00&lt;/matchvalue&gt;&lt;matchvaluerep&gt;221.00&lt;/matchvaluerep&gt;&lt;matchvaluecur&gt;221.00&lt;/matchvaluecur&gt;&lt;/line&gt;&lt;line result="1"&gt;&lt;transcode name="Pintransacties" shortname="Pin"&gt;PIN&lt;/transcode&gt;&lt;transnumber&gt;202200004&lt;/transnumber&gt;&lt;transline&gt;2&lt;/transline&gt;&lt;origin&gt;import&lt;/origin&gt;&lt;status&gt;final&lt;/status&gt;&lt;dim1&gt;1300&lt;/dim1&gt;&lt;dim2&gt;1003&lt;/dim2&gt;&lt;matchlevel&gt;2&lt;/matchlevel&gt;&lt;basevalueopen&gt;-221.00&lt;/basevalueopen&gt;&lt;repvalueopen&gt;-221.00&lt;/repvalueopen&gt;&lt;valueopen&gt;-221.00&lt;/valueopen&gt;&lt;matchvalue&gt;-221.00&lt;/matchvalue&gt;&lt;matchvaluerep&gt;-221.00&lt;/matchvaluerep&gt;&lt;matchvaluecur&gt;-221.00&lt;/matchvaluecur&gt;&lt;/line&gt;&lt;/lines&gt;&lt;dimensions&gt;&lt;dimension level="2" matchnumber="2"&gt;1003&lt;/dimension&gt;&lt;/dimensions&gt;&lt;value&gt;0.00&lt;/value&gt;&lt;/set&gt;&lt;/match&gt;</ProcessXmlStringResult></ProcessXmlStringResponse></soap:Body></soap:Envelope>' }
126
+
127
+ before do
128
+ stub_create_session
129
+ stub_cluster_session_wsdl
130
+ stub_select_company
131
+ end
132
+
133
+ it "raises an error when they don't match" do
134
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
135
+ .to_return(body: error_body)
136
+
137
+ expect { Twinfield::Browse::Transaction::Customer.new(value: 60.5, code: "VRK").match!(payment_transaction) }.to raise_error(Twinfield::TransactionMatchError)
138
+ end
139
+
140
+ it "returns the match when they match" do
141
+ stub_request(:post, "https://accounting.twinfield.com/webservices/processxml.asmx")
142
+ .to_return(body: success_body)
143
+
144
+ Twinfield::Browse::Transaction::Customer.new(value: 60.5, code: "VRK").match!(payment_transaction)
145
+
146
+ # nothing is really stored in the match it seems; but side effect is that the transactions change from available to matched
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,24 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "twinfield/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "twinfieldrb"
6
+ s.version = Twinfield::Version::VERSION
7
+ s.authors = ["Ernst Rijsdijk", "Stephan van Diepen", "Joris Reijrink", "Maarten Brouwers"]
8
+ s.email = ["ernst.rijsdijk@holder.nl", "s.vandiepen@noxa.nl", "joris@sprintict.nl", "maarten@murb.nl"]
9
+ s.homepage = "https://github.com/murb/twinfield"
10
+ s.summary = "A simple client for the Twinfield SOAP-based API (continuation of the twinfield gem)"
11
+ s.description = "Twinfield is an international Web service for collaborative online accounting. The Twinfield gem is a simple client for their SOAP-based API."
12
+
13
+ # s.rubyforge_project = "twinfield"
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_runtime_dependency "savon", "~> 2.10"
20
+ s.add_runtime_dependency "nokogiri", "~> 1.6"
21
+ s.add_development_dependency "rspec"
22
+ s.add_development_dependency "standard"
23
+ s.add_development_dependency "webmock"
24
+ end