twinfieldrb 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/codeql-analysis.yml +70 -0
- data/.github/workflows/rspec.yml +33 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +4 -0
- data/README.md +120 -0
- data/Rakefile +1 -0
- data/lib/twinfield/abstract_model.rb +7 -0
- data/lib/twinfield/api/base_api.rb +50 -0
- data/lib/twinfield/api/finder.rb +45 -0
- data/lib/twinfield/api/o_auth_session.rb +58 -0
- data/lib/twinfield/api/process.rb +44 -0
- data/lib/twinfield/api/session.rb +170 -0
- data/lib/twinfield/browse/transaction/cost_center.rb +145 -0
- data/lib/twinfield/browse/transaction/customer.rb +413 -0
- data/lib/twinfield/browse/transaction/general_ledger.rb +144 -0
- data/lib/twinfield/configuration.rb +49 -0
- data/lib/twinfield/create/cost_center.rb +39 -0
- data/lib/twinfield/create/creditor.rb +88 -0
- data/lib/twinfield/create/debtor.rb +88 -0
- data/lib/twinfield/create/error.rb +30 -0
- data/lib/twinfield/create/general_ledger.rb +39 -0
- data/lib/twinfield/create/transaction.rb +97 -0
- data/lib/twinfield/customer.rb +612 -0
- data/lib/twinfield/helpers/parsers.rb +23 -0
- data/lib/twinfield/helpers/transaction_match.rb +40 -0
- data/lib/twinfield/request/find.rb +149 -0
- data/lib/twinfield/request/list.rb +66 -0
- data/lib/twinfield/request/read.rb +111 -0
- data/lib/twinfield/sales_invoice.rb +409 -0
- data/lib/twinfield/transaction.rb +112 -0
- data/lib/twinfield/version.rb +5 -0
- data/lib/twinfield.rb +89 -0
- data/script/boot.rb +58 -0
- data/script/console +2 -0
- data/spec/fixtures/cluster/finder/ivt.xml +1 -0
- data/spec/fixtures/cluster/processxml/columns/sales_transactions.xml +312 -0
- data/spec/fixtures/cluster/processxml/customer/create_success.xml +100 -0
- data/spec/fixtures/cluster/processxml/customer/read_success.xml +93 -0
- data/spec/fixtures/cluster/processxml/customer/update_success.xml +1 -0
- data/spec/fixtures/cluster/processxml/invoice/create_error.xml +8 -0
- data/spec/fixtures/cluster/processxml/invoice/create_final_error.xml +67 -0
- data/spec/fixtures/cluster/processxml/invoice/create_success.xml +64 -0
- data/spec/fixtures/cluster/processxml/invoice/read_not_found.xml +1 -0
- data/spec/fixtures/cluster/processxml/invoice/read_success.xml +106 -0
- data/spec/fixtures/cluster/processxml/read/deb.xml +12 -0
- data/spec/fixtures/cluster/processxml/response.xml +8 -0
- data/spec/fixtures/login/session/wsdl.xml +210 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/stubs/finder_stubs.rb +19 -0
- data/spec/stubs/processxml_stubs.rb +41 -0
- data/spec/stubs/session_stubs.rb +28 -0
- data/spec/twinfield/api/oauth_session_spec.rb +37 -0
- data/spec/twinfield/api/process_spec.rb +7 -0
- data/spec/twinfield/browse/transaction/cost_center_spec.rb +60 -0
- data/spec/twinfield/browse/transaction/general_ledger_spec.rb +60 -0
- data/spec/twinfield/browse/transaction/transaction_spec.rb +72 -0
- data/spec/twinfield/config_spec.rb +60 -0
- data/spec/twinfield/customer_spec.rb +326 -0
- data/spec/twinfield/request/find_spec.rb +24 -0
- data/spec/twinfield/request/list_spec.rb +58 -0
- data/spec/twinfield/request/read_spec.rb +26 -0
- data/spec/twinfield/sales_invoice_spec.rb +253 -0
- data/spec/twinfield/session_spec.rb +77 -0
- data/spec/twinfield/transaction_spec.rb +149 -0
- data/twinfieldrb.gemspec +24 -0
- data/wsdls/accounting/finder.wsdl +157 -0
- data/wsdls/accounting/process.wsdl +199 -0
- data/wsdls/accounting/session.wsdl +452 -0
- data/wsdls/accounting2/finder.wsdl +157 -0
- data/wsdls/accounting2/process.wsdl +199 -0
- data/wsdls/api.accounting/finder.wsdl +157 -0
- data/wsdls/api.accounting/process.wsdl +199 -0
- data/wsdls/session.wsdl +210 -0
- data/wsdls/update +10 -0
- metadata +196 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e7198d5041d60aeaa7150c2175b914f55a6f712eeed2607ea417bbfd59a40619
|
4
|
+
data.tar.gz: da01ddd6abd5bbf424b7caf8d124a270ab70f428c0a81e1359c2a9633b4e975b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b12687672b183120a1d21e6d931618993802da9ca80ded48b5a951b232a8e7e431e79c0af83199adb784ec4c7723dbf927615d4ecc4585ae76e6bc75c2bc65d7
|
7
|
+
data.tar.gz: 42c6c9be13b1e1fa4a07d49813504e1bd632d85c500476a8377412abde3a0d94cb11b8aa299760bb85e33eda3e4797a9ec1ab4039cda9e7cd0019bee971244ba
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# For most projects, this workflow file will not need changing; you simply need
|
2
|
+
# to commit it to your repository.
|
3
|
+
#
|
4
|
+
# You may wish to alter this file to override the set of languages analyzed,
|
5
|
+
# or to provide custom queries or build logic.
|
6
|
+
#
|
7
|
+
# ******** NOTE ********
|
8
|
+
# We have attempted to detect the languages in your repository. Please check
|
9
|
+
# the `language` matrix defined below to confirm you have the correct set of
|
10
|
+
# supported CodeQL languages.
|
11
|
+
#
|
12
|
+
name: "CodeQL"
|
13
|
+
|
14
|
+
on:
|
15
|
+
push:
|
16
|
+
branches: [ master ]
|
17
|
+
pull_request:
|
18
|
+
# The branches below must be a subset of the branches above
|
19
|
+
branches: [ master ]
|
20
|
+
schedule:
|
21
|
+
- cron: '23 6 * * 2'
|
22
|
+
|
23
|
+
jobs:
|
24
|
+
analyze:
|
25
|
+
name: Analyze
|
26
|
+
runs-on: ubuntu-latest
|
27
|
+
permissions:
|
28
|
+
actions: read
|
29
|
+
contents: read
|
30
|
+
security-events: write
|
31
|
+
|
32
|
+
strategy:
|
33
|
+
fail-fast: false
|
34
|
+
matrix:
|
35
|
+
language: [ 'ruby' ]
|
36
|
+
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
37
|
+
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
38
|
+
|
39
|
+
steps:
|
40
|
+
- name: Checkout repository
|
41
|
+
uses: actions/checkout@v2
|
42
|
+
|
43
|
+
# Initializes the CodeQL tools for scanning.
|
44
|
+
- name: Initialize CodeQL
|
45
|
+
uses: github/codeql-action/init@v1
|
46
|
+
with:
|
47
|
+
languages: ${{ matrix.language }}
|
48
|
+
# If you wish to specify custom queries, you can do so here or in a config file.
|
49
|
+
# By default, queries listed here will override any specified in a config file.
|
50
|
+
# Prefix the list here with "+" to use these queries and those in the config file.
|
51
|
+
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
52
|
+
|
53
|
+
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
54
|
+
# If this step fails, then you should remove it and run the build manually (see below)
|
55
|
+
- name: Autobuild
|
56
|
+
uses: github/codeql-action/autobuild@v1
|
57
|
+
|
58
|
+
# ℹ️ Command-line programs to run using the OS shell.
|
59
|
+
# 📚 https://git.io/JvXDl
|
60
|
+
|
61
|
+
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
62
|
+
# and modify them (or add more) to build your code if your project
|
63
|
+
# uses a compiled language
|
64
|
+
|
65
|
+
#- run: |
|
66
|
+
# make bootstrap
|
67
|
+
# make release
|
68
|
+
|
69
|
+
- name: Perform CodeQL Analysis
|
70
|
+
uses: github/codeql-action/analyze@v1
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
2
|
+
# They are provided by a third-party and are governed by
|
3
|
+
# separate terms of service, privacy policy, and support
|
4
|
+
# documentation.
|
5
|
+
|
6
|
+
name: Ruby CI
|
7
|
+
|
8
|
+
on:
|
9
|
+
push:
|
10
|
+
branches: [ main, master ]
|
11
|
+
pull_request:
|
12
|
+
branches: [ main, master ]
|
13
|
+
|
14
|
+
jobs:
|
15
|
+
test:
|
16
|
+
|
17
|
+
runs-on: ubuntu-24.04
|
18
|
+
|
19
|
+
strategy:
|
20
|
+
matrix:
|
21
|
+
ruby-version: [ 3.1.6, 3.2.6, 3.3.6]
|
22
|
+
|
23
|
+
steps:
|
24
|
+
- uses: actions/checkout@v2
|
25
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
26
|
+
uses: ruby/setup-ruby@v1
|
27
|
+
with:
|
28
|
+
bundler-cache: true
|
29
|
+
ruby-version: ${{ matrix.ruby-version }}
|
30
|
+
- name: Install dependencies
|
31
|
+
run: bundle install
|
32
|
+
- name: Run tests
|
33
|
+
run: bundle exec rspec
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All changes to the project will be documented in this file.
|
4
|
+
|
5
|
+
## Pending release
|
6
|
+
|
7
|
+
* [BREAKING] Ruby > 3
|
8
|
+
* [BREAKING] Invoices are now what Twinfield calls SalesInvoices, not SalesTransactions
|
9
|
+
* [FEATURE] Added different classes that act more like ruby native objects in the Twinfield domain, such as Twinfield::Customer, Twinfield::Invoice, Twinfield::Transaction and Twinfield::Browse::Transaction::Customer
|
10
|
+
* [FEATURE] Allowed for OAuthSession; which is the preferred way of creating sessions
|
11
|
+
* [BUGFIX] Finder code made to work
|
12
|
+
|
13
|
+
## 0.0.x
|
14
|
+
|
15
|
+
Initial releases by Ernst Rijsdijk, Stephan van Diepen and Joris Reijrink.
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
# TwinfieldRB
|
2
|
+
|
3
|
+
Twinfield is an international Web service for collaborative online accounting. The `twinfieldrb` gem is a simple client for their SOAP-based API (it is a continuation of the long forgotten `twinfield` gem)
|
4
|
+
|
5
|
+
This project started as a clone of the `twinfield`-gem, but [I've been working](CHANGELOG.md) to abstract the soap calls away. Perhaps I'll release this as a gem on its own. For now see "Installing > Using Twinfield".
|
6
|
+
|
7
|
+
## Installing
|
8
|
+
|
9
|
+
### Using Twinfield
|
10
|
+
|
11
|
+
Add Twinfield in `Gemfile` as a gem dependency:
|
12
|
+
|
13
|
+
gem "twinfieldrb", git: "https://github.com/murb/twinfield.git"
|
14
|
+
|
15
|
+
Run the following in your console to install with Bundler:
|
16
|
+
|
17
|
+
bundle install
|
18
|
+
|
19
|
+
For OAuth authentication, now the default As a companion gem a [`omniauth-twinfield`-gem](https://github.com/murb/omniauth-twinfield) was created to help setting up OAuth.
|
20
|
+
|
21
|
+
## Configuration
|
22
|
+
|
23
|
+
Your application will authenticate to Twinfield using OAuth. A companion gem was created to help setting up OAuth communication with your app: [`omniauth-twinfield`-gem](https://github.com/murb/omniauth-twinfield).
|
24
|
+
|
25
|
+
Cluster and access token are retrieved in the OAuth flow:
|
26
|
+
|
27
|
+
Twinfield.configure do |config|
|
28
|
+
config.session_type = "Twinfield::Api::OAuthSession"
|
29
|
+
config.cluster = OAuthClient.last.token_data["twf.clusterUrl"]
|
30
|
+
config.access_token = OAuthClient.first.access_token
|
31
|
+
config.log_level = :debug
|
32
|
+
end
|
33
|
+
|
34
|
+
Twinfield::Request::List.offices
|
35
|
+
|
36
|
+
Now configure the company you're looking for:
|
37
|
+
|
38
|
+
Twinfield.configuration.company="NL123"
|
39
|
+
|
40
|
+
## Examples
|
41
|
+
|
42
|
+
Here are some examples that may be useful when using this GEM for the first time.
|
43
|
+
|
44
|
+
### List of all debtors
|
45
|
+
|
46
|
+
Twinfield::Request::List.dimensions({ dimtype: "DEB" })
|
47
|
+
|
48
|
+
### Create a new debtor
|
49
|
+
|
50
|
+
debtor = Twinfield::Customer.new
|
51
|
+
debtor.code = Twinfield::Customer.next_unused_twinfield_customer_code # sorry
|
52
|
+
debtor.name = ""
|
53
|
+
debtor.shortname = ""
|
54
|
+
debtor.country = ""
|
55
|
+
debtor.banks = [Twinfield::Customer::Bank.new]
|
56
|
+
debtor.save
|
57
|
+
|
58
|
+
### Create a new invoice
|
59
|
+
|
60
|
+
invoice = Twinfield::SalesInvoice.new(customer: 1003, invoicetype: "FACTUUR", currency: "EUR", invoicedate: Time.now, duedate: Time.now+1.month)
|
61
|
+
invoice.lines << Twinfield::SalesInvoice::Line.new(article: "A", unitspriceexcl: 100, allowdiscountorpremium: true, vatcode: "VH")
|
62
|
+
invoice.lines << Twinfield::SalesInvoice::Line.new(article: 0, unitspriceexcl: 100, description: "Spaartegoed", allowdiscountorpremium: true, vatcode: "VN")
|
63
|
+
invoice.lines << Twinfield::SalesInvoice::Line.new(article: 0, unitspriceexcl: 0, quantity: 0, description: "Custom article", allowdiscountorpremium: true, vatcode: "VH")
|
64
|
+
|
65
|
+
invoice.save
|
66
|
+
|
67
|
+
### Read an invoice
|
68
|
+
|
69
|
+
The following should be enough to
|
70
|
+
|
71
|
+
invoice = Twinfield::SalesInvoice.find(12, invoicetype: "FACTUUR")
|
72
|
+
invoice.lines # returns the lines
|
73
|
+
invoice.vat_lines # returns the vat lines
|
74
|
+
invoice.total # returns the total amount (derived)
|
75
|
+
invoice.customer # returns an Twinfield::Customer
|
76
|
+
invoice.invoice_address # returns the customer's invoice address (Twinfield::Customer::Address, not just a code)
|
77
|
+
invoice.delivery_address # returns the full delivery address (Twinfield::Customer::Address, not just a code)
|
78
|
+
|
79
|
+
The matching transaction can be found using:
|
80
|
+
|
81
|
+
invoice.transaction
|
82
|
+
|
83
|
+
### Find a transaction
|
84
|
+
|
85
|
+
transactions = Twinfield::Browse::Transaction::Customer.where(invoice_number: 12, code: "VRK")
|
86
|
+
|
87
|
+
### Register a payment
|
88
|
+
|
89
|
+
trans = Twinfield::Transaction.new(code: "PIN", currency: "EUR")
|
90
|
+
trans.lines << Twinfield::Transaction::Line.new(type: :total, balance_code: "1230", value: 0.0, debitcredit: :debit)
|
91
|
+
trans.lines << Twinfield::Transaction::Line.new(type: :detail, balance_code: 1300, value: 60.5, debitcredit: :credit, customer_code: 1003, invoicenumber: 14)
|
92
|
+
trans.lines << Twinfield::Transaction::Line.new(type: :detail, balance_code: 1234, value: 60.5, debitcredit: :debit)
|
93
|
+
trans.save
|
94
|
+
|
95
|
+
Now this payment can be matched against the invoice (if amounts match)):
|
96
|
+
|
97
|
+
trans.match!(Twinfield::Browse::Transaction::Customer.find(invoice_number: 14, code: "VRK"))
|
98
|
+
|
99
|
+
### Get the transactions for a customer
|
100
|
+
|
101
|
+
This gets a list of sales transactions for customer with code 1003
|
102
|
+
|
103
|
+
customer = Twinfield::Customer.find(1003)
|
104
|
+
customer.transactions(code: "VRK")
|
105
|
+
|
106
|
+
### Store collect mandate
|
107
|
+
|
108
|
+
customer = Twinfield::Customer.find(1003)
|
109
|
+
customer.financials.collectmandate = Twinfield::Customer::CollectMandate.new(signaturedate: signed_on, id: sepa_proof_uuid)
|
110
|
+
customer.financials.payavailable = true # autosets meansofpayment to paymentfile; can be disabled to temporarily stop collecting through paymentfiles
|
111
|
+
customer.financials.paycode="SEPANLDD" # has to correspond to the countries default SEPA paymentfile
|
112
|
+
|
113
|
+
### List office
|
114
|
+
|
115
|
+
Twinfield::Request::Read.office
|
116
|
+
|
117
|
+
## Known issues
|
118
|
+
|
119
|
+
* The way configuration works may not work well in concurrent environments with multiple clients
|
120
|
+
* The latest development and use in practice has been using the OAuth-based approach; the old Twinfield::Api::Session may not even work anymore
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Twinfield
|
2
|
+
module Api
|
3
|
+
class BaseApi
|
4
|
+
class << self
|
5
|
+
def session
|
6
|
+
@session ||= Twinfield.configuration.session_class.new
|
7
|
+
@session.logon
|
8
|
+
@session
|
9
|
+
end
|
10
|
+
|
11
|
+
def session= session
|
12
|
+
@client = nil
|
13
|
+
@session = session
|
14
|
+
end
|
15
|
+
|
16
|
+
def client
|
17
|
+
options = {
|
18
|
+
wsdl: wsdl,
|
19
|
+
env_namespace: :soap,
|
20
|
+
encoding: "UTF-8",
|
21
|
+
namespace_identifier: nil,
|
22
|
+
log: !!Twinfield.configuration.log_level,
|
23
|
+
log_level: Twinfield.configuration.log_level || :info
|
24
|
+
}
|
25
|
+
options[:logger] = Twinfield.configuration.logger if Twinfield.configuration.logger
|
26
|
+
|
27
|
+
@client ||= Savon.client(options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def wsdl
|
31
|
+
raise "undefined .wsdl"
|
32
|
+
end
|
33
|
+
|
34
|
+
def cluster
|
35
|
+
session.cluster
|
36
|
+
end
|
37
|
+
|
38
|
+
def cluster_short_name
|
39
|
+
if cluster.match?("accounting2.")
|
40
|
+
"accounting2"
|
41
|
+
elsif cluster.match?("api.accounting")
|
42
|
+
"api.accounting"
|
43
|
+
else
|
44
|
+
"accounting"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Twinfield
|
2
|
+
module Api
|
3
|
+
class Finder < BaseApi
|
4
|
+
class << self
|
5
|
+
def wsdl
|
6
|
+
Twinfield::WSDLS[cluster_short_name][:finder]
|
7
|
+
end
|
8
|
+
|
9
|
+
def actions
|
10
|
+
@actions ||= client.operations
|
11
|
+
end
|
12
|
+
|
13
|
+
# request
|
14
|
+
# see: https://accounting.twinfield.com/webservices/documentation/#/ApiReference/Transactions/SalesInvoices
|
15
|
+
def request(type, options = {})
|
16
|
+
Twinfield.configuration.logger&.debug(" ↳ #{caller.find { |a| !a.match(/\/gems\/|\/ruby\/|<internal:/) }}")
|
17
|
+
|
18
|
+
first_row = options.delete(:first_row) || 1
|
19
|
+
pattern = options.delete(:pattern) || "*"
|
20
|
+
max_rows = options.delete(:max_rows) || 100
|
21
|
+
|
22
|
+
message = {
|
23
|
+
"type" => type,
|
24
|
+
"pattern" => pattern,
|
25
|
+
"field" => "0",
|
26
|
+
"firstRow" => first_row,
|
27
|
+
"maxRows" => max_rows,
|
28
|
+
"options" => {
|
29
|
+
"ArrayOfString" => options.map { |k, v| {"string" => [k, v]} }
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
xml = client.operation(:search).build(attributes: {xmlns: "http://www.twinfield.com/"}, soap_header: session.header, message: message).build_document
|
34
|
+
|
35
|
+
client.call(:search, xml: strip_global_namespace_from_xml(xml))
|
36
|
+
end
|
37
|
+
|
38
|
+
def strip_global_namespace_from_xml xml
|
39
|
+
# ugly enough xmlns is prefixed even though it suggests an element in the global document's namespace
|
40
|
+
xml.gsub("<xmlns:", "<").gsub("</xmlns:", "</")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Twinfield
|
2
|
+
module Api
|
3
|
+
class OAuthSession
|
4
|
+
HEADER_TEMPLATE = {
|
5
|
+
"Header" => {},
|
6
|
+
:attributes! => {
|
7
|
+
"Header" => {xmlns: "http://www.twinfield.com/"}
|
8
|
+
}
|
9
|
+
}
|
10
|
+
|
11
|
+
attr_accessor :cluster, :access_token
|
12
|
+
|
13
|
+
# sets up a new savon client which will be used for current Session
|
14
|
+
def initialize
|
15
|
+
@cluster = Twinfield.configuration.cluster
|
16
|
+
@access_token = Twinfield.configuration.access_token
|
17
|
+
end
|
18
|
+
|
19
|
+
# retrieve a session_id and cluster from twinfield
|
20
|
+
# relog is by default set to false so when logon is called on your
|
21
|
+
# current session, you wont lose your session_id
|
22
|
+
def logon(relog = false)
|
23
|
+
# no need to logon with OAuth
|
24
|
+
end
|
25
|
+
|
26
|
+
# call logon method with relog set to true
|
27
|
+
# this wil destroy the current session and cluster
|
28
|
+
def relog
|
29
|
+
logon(true)
|
30
|
+
end
|
31
|
+
|
32
|
+
# after a logon try you can ask the current status
|
33
|
+
def status
|
34
|
+
if connected?
|
35
|
+
"Ok"
|
36
|
+
else
|
37
|
+
"No access token"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns true or false if current session has a session_id
|
42
|
+
# and cluster from twinfield
|
43
|
+
def connected?
|
44
|
+
!!@access_token && !!@cluster
|
45
|
+
end
|
46
|
+
|
47
|
+
def header
|
48
|
+
soap_header = HEADER_TEMPLATE
|
49
|
+
|
50
|
+
header_contents = {}
|
51
|
+
header_contents["AccessToken"] = access_token if access_token
|
52
|
+
header_contents["CompanyCode"] = Twinfield.configuration.company if Twinfield.configuration.company
|
53
|
+
|
54
|
+
soap_header.merge({"Header" => header_contents})
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Twinfield
|
2
|
+
module Api
|
3
|
+
class Process < BaseApi
|
4
|
+
class << self
|
5
|
+
def wsdl
|
6
|
+
Twinfield::WSDLS[cluster_short_name][:process]
|
7
|
+
end
|
8
|
+
|
9
|
+
def actions
|
10
|
+
@actions ||= client.operations
|
11
|
+
end
|
12
|
+
|
13
|
+
def request(action = :process_xml_string, options = {}, &block)
|
14
|
+
Twinfield.configuration.logger&.debug(" ↳ #{caller.find { |a| !a.match(/\/gems\/|\/ruby\/|<internal:/) }}")
|
15
|
+
|
16
|
+
if actions.include?(action)
|
17
|
+
message = "<xmlRequest><![CDATA[#{block.call}]]></xmlRequest>"
|
18
|
+
|
19
|
+
client.call(action, attributes: {xmlns: "http://www.twinfield.com/"}, soap_header: session.header, message: message)
|
20
|
+
else
|
21
|
+
"action not found"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def read(element, options)
|
26
|
+
response = Twinfield::Api::Process.request(:process_xml_string) do
|
27
|
+
%(
|
28
|
+
<read>
|
29
|
+
<type>#{element}</type>
|
30
|
+
#{Twinfield::Api::Process.options_to_xml(options)}
|
31
|
+
</read>
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
Nokogiri::XML(response.body[:process_xml_string_response][:process_xml_string_result])
|
36
|
+
end
|
37
|
+
|
38
|
+
def options_to_xml(options)
|
39
|
+
options.map { |k, v| "<#{k}>#{v}</#{k}>" }.join("\n")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module Twinfield
|
2
|
+
module Api
|
3
|
+
class Session
|
4
|
+
HEADER_TEMPLATE = {
|
5
|
+
"Header" => {},
|
6
|
+
:attributes! => {
|
7
|
+
"Header" => {xmlns: "http://www.twinfield.com/"}
|
8
|
+
}
|
9
|
+
}
|
10
|
+
|
11
|
+
attr_accessor :session_id, :cluster
|
12
|
+
|
13
|
+
# sets up a new savon client which will be used for current Session
|
14
|
+
def initialize
|
15
|
+
@client = Savon.client(wsdl: Twinfield::WSDLS[:session],
|
16
|
+
log: !!Twinfield.configuration.log_level,
|
17
|
+
log_level: Twinfield.configuration.log_level || :info)
|
18
|
+
end
|
19
|
+
|
20
|
+
# retrieve a session_id and cluster from twinfield
|
21
|
+
# relog is by default set to false so when logon is called on your
|
22
|
+
# current session, you wont lose your session_id
|
23
|
+
def logon(relog = false)
|
24
|
+
if connected? && (relog == false)
|
25
|
+
"already connected"
|
26
|
+
else
|
27
|
+
response = @client.call(:logon, message: Twinfield.configuration.to_logon_hash)
|
28
|
+
|
29
|
+
if response.body[:logon_response][:logon_result] == "Ok"
|
30
|
+
@session_id = response.header[:header][:session_id]
|
31
|
+
@cluster = response.body[:logon_response][:cluster]
|
32
|
+
|
33
|
+
select_company(Twinfield.configuration.company)
|
34
|
+
end
|
35
|
+
|
36
|
+
@message = response.body[:logon_response][:logon_result]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# call logon method with relog set to true
|
41
|
+
# this wil destroy the current session and cluster
|
42
|
+
def relog
|
43
|
+
logon(true)
|
44
|
+
end
|
45
|
+
|
46
|
+
# after a logon try you can ask the current status
|
47
|
+
def status
|
48
|
+
@message
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns true or false if current session has a session_id
|
52
|
+
# and cluster from twinfield
|
53
|
+
def connected?
|
54
|
+
!!@session_id && !!@cluster
|
55
|
+
end
|
56
|
+
|
57
|
+
# Abandons the session.
|
58
|
+
def abandon
|
59
|
+
if session_id
|
60
|
+
message = "<Abandon xmlns='http://www.twinfield.com/' />"
|
61
|
+
@client.call(:Abandon, attributes: {xmlns: "http://www.twinfield.com/"}, soap_header: header, message: message)
|
62
|
+
|
63
|
+
# TODO: Return real status
|
64
|
+
# There is no message from twinfield if the action succeeded
|
65
|
+
"Ok"
|
66
|
+
else
|
67
|
+
"no session found"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Keep the session alive, to prevent session time out. A session time out will occur after 20 minutes.
|
72
|
+
def keep_alive
|
73
|
+
@client.request :keep_alive do
|
74
|
+
soap.header = header
|
75
|
+
soap.body = "<KeepAliveResponse xmlns='http://www.twinfield.com/' />"
|
76
|
+
end
|
77
|
+
# TODO: Return real status
|
78
|
+
# There is no message from twinfield if the action succeeded
|
79
|
+
"Ok"
|
80
|
+
end
|
81
|
+
|
82
|
+
def header
|
83
|
+
soap_header = HEADER_TEMPLATE
|
84
|
+
soap_header = soap_header.merge({"Header" => {"SessionID" => session_id}}) if session_id
|
85
|
+
soap_header
|
86
|
+
end
|
87
|
+
|
88
|
+
# Gets the session's user role.
|
89
|
+
def get_role
|
90
|
+
# <?xml version="1.0" encoding="utf-8"?>
|
91
|
+
# <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
92
|
+
# <soap:Header>
|
93
|
+
# <Header xmlns="http://www.twinfield.com/">
|
94
|
+
# <SessionID>string</SessionID>
|
95
|
+
# </Header>
|
96
|
+
# </soap:Header>
|
97
|
+
# <soap:Body>
|
98
|
+
# <GetRole xmlns="http://www.twinfield.com/" />
|
99
|
+
# </soap:Body>
|
100
|
+
# </soap:Envelope>
|
101
|
+
raise NotImplementedError
|
102
|
+
end
|
103
|
+
|
104
|
+
# Sends the sms code.
|
105
|
+
def sms_send_code
|
106
|
+
# <?xml version="1.0" encoding="utf-8"?>
|
107
|
+
# <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
108
|
+
# <soap:Header>
|
109
|
+
# <Header xmlns="http://www.twinfield.com/">
|
110
|
+
# <SessionID>string</SessionID>
|
111
|
+
# </Header>
|
112
|
+
# </soap:Header>
|
113
|
+
# <soap:Body>
|
114
|
+
# <SmsSendCode xmlns="http://www.twinfield.com/" />
|
115
|
+
# </soap:Body>
|
116
|
+
# </soap:Envelope>
|
117
|
+
raise NotImplementedError
|
118
|
+
end
|
119
|
+
|
120
|
+
# Logs on with the sms code.
|
121
|
+
def sms_logon
|
122
|
+
# <?xml version="1.0" encoding="utf-8"?>
|
123
|
+
# <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
124
|
+
# <soap:Header>
|
125
|
+
# <Header xmlns="http://www.twinfield.com/">
|
126
|
+
# <SessionID>string</SessionID>
|
127
|
+
# </Header>
|
128
|
+
# </soap:Header>
|
129
|
+
# <soap:Body>
|
130
|
+
# <SmsLogon xmlns="http://www.twinfield.com/">
|
131
|
+
# <smsCode>string</smsCode>
|
132
|
+
# </SmsLogon>
|
133
|
+
# </soap:Body>
|
134
|
+
# </soap:Envelope>
|
135
|
+
raise NotImplementedError
|
136
|
+
end
|
137
|
+
|
138
|
+
# Changes the password.
|
139
|
+
def change_password
|
140
|
+
# <?xml version="1.0" encoding="utf-8"?>
|
141
|
+
# <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
142
|
+
# <soap:Header>
|
143
|
+
# <Header xmlns="http://www.twinfield.com/">
|
144
|
+
# <SessionID>string</SessionID>
|
145
|
+
# </Header>
|
146
|
+
# </soap:Header>
|
147
|
+
# <soap:Body>
|
148
|
+
# <ChangePassword xmlns="http://www.twinfield.com/">
|
149
|
+
# <currentPassword>string</currentPassword>
|
150
|
+
# <newPassword>string</newPassword>
|
151
|
+
# </ChangePassword>
|
152
|
+
# </soap:Body>
|
153
|
+
# </soap:Envelope>
|
154
|
+
raise NotImplementedError
|
155
|
+
end
|
156
|
+
|
157
|
+
# Selects a company.
|
158
|
+
def select_company(code)
|
159
|
+
message = "<company>#{code}</company>"
|
160
|
+
|
161
|
+
Savon.client(wsdl: "#{@cluster}/webservices/session.asmx?wsdl",
|
162
|
+
env_namespace: :soap,
|
163
|
+
encoding: "UTF-8",
|
164
|
+
namespace_identifier: nil).call(:select_company, attributes: {xmlns: "http://www.twinfield.com/"}, soap_header: header, message: message)
|
165
|
+
|
166
|
+
"Ok"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|