spree-kashflow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "savon"
4
+
5
+ module Spree
6
+ module Kashflow
7
+ ##
8
+ # Stateless SOAP client for the KashFlow API. Only this class may reference Savon —
9
+ # every public method returns plain Ruby (Integer, Array<Hash>, TrueClass) so no SOAP
10
+ # object crosses the gem boundary. KashFlow has no session token: credentials are sent
11
+ # on every call, so nothing is cached between calls.
12
+ #
13
+ class Client
14
+ # @return [String] the WSDL endpoint KashFlow publishes for the SOAP API
15
+ WSDL = "https://securedwebapp.com/api/service.asmx?WSDL"
16
+
17
+ # @return [Regexp] matches SOAP faults caused by bad credentials
18
+ AUTH_FAULT = /invalid.*(username|password)|not authori[sz]ed/i
19
+
20
+ # @return [String] the in-band `Status` value KashFlow returns on success.
21
+ #
22
+ # The WSDL declares `Status` as a bare `s:string` on every `*Response`
23
+ # element (`minOccurs="0"`), with no `<s:enumeration>` and no
24
+ # `<wsdl:documentation>` naming the success value — so this constant is
25
+ # KashFlow's documented convention (`"OK"`), not something the schema
26
+ # pins down. Comparison is case-insensitive, and an *absent* `Status`
27
+ # (legal, since `minOccurs="0"`) is treated as success; only a present
28
+ # `Status` that is not `"OK"` is a business-level rejection.
29
+ SUCCESS_STATUS = "OK"
30
+
31
+ ##
32
+ # @param username [String] the KashFlow API username
33
+ # @param password [String] the KashFlow API password
34
+ #
35
+ def initialize(username:, password:)
36
+ @username = username
37
+ @password = password
38
+ end
39
+
40
+ ##
41
+ # Verifies the configured credentials against the KashFlow API. Does not rescue —
42
+ # the caller decides how to handle a raised error.
43
+ #
44
+ # @return [TrueClass, FalseClass] true when KashFlow accepts the credentials
45
+ # @raise [Spree::Kashflow::Error] when KashFlow rejects the credentials or the
46
+ # request otherwise fails
47
+ #
48
+ def verify_credentials
49
+ call(:get_currencies)
50
+ true
51
+ end
52
+
53
+ ##
54
+ # @return [Array<Hash>] currencies as `{code:, id:}` hashes
55
+ # @raise [Spree::Kashflow::Error] when the request fails
56
+ #
57
+ def currencies
58
+ rows = extract_rows(call(:get_currencies), "GetCurrenciesResponse", "GetCurrenciesResult", "Currencies")
59
+ rows.map { |row| {code: row["CurrencyCode"], id: row["CurrencyId"].to_i} }
60
+ end
61
+
62
+ ##
63
+ # @return [Array<Hash>] nominal codes as `{id:, name:}` hashes
64
+ # @raise [Spree::Kashflow::Error] when the request fails
65
+ #
66
+ def nominal_codes
67
+ rows = extract_rows(call(:get_nominal_codes), "GetNominalCodesResponse", "GetNominalCodesResult", "NominalCode")
68
+ rows.map { |row| {id: row["id"].to_i, name: row["Name"]} }
69
+ end
70
+
71
+ ##
72
+ # @return [Array<Hash>] bank accounts as `{id:, name:}` hashes
73
+ # @raise [Spree::Kashflow::Error] when the request fails
74
+ #
75
+ def bank_accounts
76
+ rows = extract_rows(call(:get_bank_accounts), "GetBankAccountsResponse", "GetBankAccountsResult", "BankAccount")
77
+ rows.map { |row| {id: row["AccountID"].to_i, name: row["AccountName"]} }
78
+ end
79
+
80
+ ##
81
+ # @return [Array<Hash>] invoice payment methods as `{id:, name:}` hashes
82
+ # @raise [Spree::Kashflow::Error] when the request fails
83
+ #
84
+ def payment_methods
85
+ rows = extract_rows(call(:get_inv_pay_methods), "GetInvPayMethodsResponse", "GetInvPayMethodsResult", "PaymentMethod")
86
+ rows.map { |row| {id: row["MethodID"].to_i, name: row["MethodName"]} }
87
+ end
88
+
89
+ ##
90
+ # Creates or updates a customer in KashFlow.
91
+ #
92
+ # @param payload [Hash] a KashFlow `Customer` structure
93
+ # @return [Integer] the KashFlow customer id
94
+ # @raise [Spree::Kashflow::ApiError] when the request fails, or when
95
+ # KashFlow returns no usable customer id
96
+ #
97
+ def upsert_customer(payload)
98
+ response = call(:insert_customer, {"custr" => payload})
99
+ result = response.dig("InsertCustomerResponse", "InsertCustomerResult")
100
+ assert_identifier!(result, "customer id")
101
+ end
102
+
103
+ ##
104
+ # Creates an invoice in KashFlow.
105
+ #
106
+ # @param payload [Hash] a KashFlow `Invoice_TypeDefined` structure
107
+ # @return [Integer] the invoice number KashFlow assigned
108
+ # @raise [Spree::Kashflow::ApiError] when the request fails, or when
109
+ # KashFlow returns no usable invoice number
110
+ #
111
+ def create_invoice(payload)
112
+ response = call(:insert_invoice_type_defined, {"Inv_TD" => payload})
113
+ result = response.dig("InsertInvoice_TypeDefinedResponse", "InsertInvoice_TypeDefinedResult")
114
+ assert_identifier!(result, "invoice number")
115
+ end
116
+
117
+ ##
118
+ # Records a payment against an invoice in KashFlow.
119
+ #
120
+ # The WSDL declares `InsertInvoicePaymentResult` as an `s:int` — the id of
121
+ # the payment KashFlow created. A `0` is therefore a rejection reported
122
+ # without a `Status`, and returning `true` regardless would leave the
123
+ # invoice permanently unpaid while the caller recorded a successful sync.
124
+ #
125
+ # @param payload [Hash] a KashFlow `Payment` structure
126
+ # @return [TrueClass] true when the payment was recorded
127
+ # @raise [Spree::Kashflow::ApiError] when the request fails, or when
128
+ # KashFlow returns no payment id
129
+ #
130
+ def record_invoice_payment(payload)
131
+ response = call(:insert_invoice_payment, {"InvoicePayment" => payload})
132
+ result = response.dig("InsertInvoicePaymentResponse", "InsertInvoicePaymentResult")
133
+ assert_identifier!(result, "payment id")
134
+ true
135
+ end
136
+
137
+ ##
138
+ # Applies a credit note to an invoice in KashFlow.
139
+ #
140
+ # The WSDL declares `applyCreditNoteToInvoiceResult` as an `s:boolean`.
141
+ # A `false` is a refusal to link, and swallowing it produces exactly the
142
+ # orphan credit note {SyncRefundJob}'s precondition guard exists to
143
+ # prevent — a credit note sitting in KashFlow attached to nothing.
144
+ #
145
+ # @param credit_note_number [Integer] the KashFlow credit note id
146
+ # @param invoice_number [Integer] the KashFlow invoice id
147
+ # @return [TrueClass] true when the credit note was applied
148
+ # @raise [Spree::Kashflow::ApiError] when the request fails, or when
149
+ # KashFlow refuses to link the credit note
150
+ #
151
+ def apply_credit_note(credit_note_number:, invoice_number:)
152
+ response = call(:apply_credit_note_to_invoice, {"InvoiceID" => invoice_number, "CreditNoteID" => credit_note_number})
153
+ result = response.dig("applyCreditNoteToInvoiceResponse", "applyCreditNoteToInvoiceResult")
154
+ assert_accepted!(result, "did not apply credit note #{credit_note_number} to invoice #{invoice_number}")
155
+ true
156
+ end
157
+
158
+ private
159
+
160
+ ##
161
+ # @return [Savon::Client] a Savon client bound to the KashFlow WSDL
162
+ #
163
+ def savon_client
164
+ @savon_client ||= Savon.client(
165
+ wsdl: WSDL,
166
+ log: false,
167
+ convert_response_tags_to: ->(tag) { tag }
168
+ )
169
+ end
170
+
171
+ ##
172
+ # Invokes a KashFlow SOAP operation, merging credentials into every request, and
173
+ # maps transport errors, SOAP faults and in-band `Status` rejections onto this
174
+ # gem's error hierarchy.
175
+ #
176
+ # @param operation [Symbol] the Savon operation name
177
+ # @param message [Hash] the operation's message body, excluding credentials
178
+ # @return [Hash] the parsed response body
179
+ # @raise [Spree::Kashflow::AuthenticationError] when KashFlow rejects the credentials
180
+ # @raise [Spree::Kashflow::ApiError] when KashFlow refuses the operation
181
+ # @raise [Spree::Kashflow::TransportError] when the KashFlow service could not be reached
182
+ #
183
+ def call(operation, message = {})
184
+ credentials = {"UserName" => @username, "Password" => @password}
185
+ response = savon_client.call(operation, message: credentials.merge(message))
186
+ body = response.body
187
+ assert_status!(body)
188
+ body
189
+ rescue Savon::SOAPFault => e
190
+ fault_message = e.to_hash.dig(:fault, :faultstring) || e.message
191
+ raise_business_error(fault_message)
192
+ rescue Savon::HTTPError, HTTPI::SSLError, OpenSSL::SSL::SSLError, SocketError,
193
+ Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EHOSTUNREACH,
194
+ Net::OpenTimeout, Net::ReadTimeout => e
195
+ raise TransportError, e.message
196
+ end
197
+
198
+ ##
199
+ # KashFlow reports business-level rejections *in band*: the SOAP call
200
+ # succeeds at HTTP 200 with an empty result element and a `Status` /
201
+ # `StatusDetail` pair beside it. Left uninspected, a rejection reads as a
202
+ # `nil` result and coerces to `0` — a value the caller would then record
203
+ # as a real KashFlow identifier. See {SUCCESS_STATUS} for why the success
204
+ # value is a convention rather than a schema-declared enumeration.
205
+ #
206
+ # @param body [Hash] the parsed response body
207
+ # @return [void]
208
+ # @raise [Spree::Kashflow::AuthenticationError] when the rejection is a
209
+ # credentials problem
210
+ # @raise [Spree::Kashflow::ApiError] for any other non-success `Status`
211
+ #
212
+ def assert_status!(body)
213
+ envelope = body.values.detect { |value| value.is_a?(Hash) }
214
+ return if envelope.nil?
215
+
216
+ status = envelope["Status"]
217
+ return if status.nil?
218
+ return if status.to_s.strip.casecmp(SUCCESS_STATUS).zero?
219
+
220
+ detail = envelope["StatusDetail"]
221
+ raise_business_error([status, detail].compact_blank.join(": "))
222
+ end
223
+
224
+ ##
225
+ # @param message [String] the message KashFlow refused the request with
226
+ # @return [void]
227
+ # @raise [Spree::Kashflow::AuthenticationError] when the message names a
228
+ # credentials problem
229
+ # @raise [Spree::Kashflow::ApiError] otherwise
230
+ #
231
+ def raise_business_error(message)
232
+ if message.match?(AUTH_FAULT)
233
+ raise AuthenticationError, message
234
+ else
235
+ raise ApiError, message
236
+ end
237
+ end
238
+
239
+ ##
240
+ # KashFlow returns `0` (or nothing at all) where it means "refused", and
241
+ # a `0` recorded as an identifier is worse than a raised error: it looks
242
+ # like a successful sync, satisfies the caller's idempotency check, and
243
+ # blocks every retry.
244
+ #
245
+ # @param result [Object, nil] the raw result element
246
+ # @param label [String] what the identifier is, for the error message
247
+ # @return [Integer] the identifier
248
+ # @raise [Spree::Kashflow::ApiError] when the result is nil or zero
249
+ #
250
+ def assert_identifier!(result, label)
251
+ identifier = result.to_i
252
+ return identifier unless result.nil? || identifier.zero?
253
+
254
+ raise ApiError, "KashFlow returned no #{label} (result: #{result.inspect})"
255
+ end
256
+
257
+ ##
258
+ # The counterpart of {#assert_identifier!} for the operations the WSDL types
259
+ # as `s:boolean` rather than `s:int`. Savon hands the element back as the
260
+ # string `"true"` / `"false"`, so only an explicit `"true"` is acceptance;
261
+ # `false`, `nil` and an absent element are all rejections.
262
+ #
263
+ # @param result [Object, nil] the raw result element
264
+ # @param message [String] what KashFlow refused, for the error message
265
+ # @return [void]
266
+ # @raise [Spree::Kashflow::ApiError] when the result is not true
267
+ #
268
+ def assert_accepted!(result, message)
269
+ return if result.to_s.strip.casecmp("true").zero?
270
+
271
+ raise ApiError, "KashFlow #{message} (result: #{result.inspect})"
272
+ end
273
+
274
+ ##
275
+ # Normalises a KashFlow list response into an array of row hashes.
276
+ #
277
+ # @param body [Hash] the parsed response body
278
+ # @param response_key [String] the top-level response element name
279
+ # @param result_key [String] the result element name nested under the response
280
+ # @param collection_key [String] the element name repeated for each row
281
+ # @return [Array<Hash>] the rows, or an empty array when the response carries none
282
+ #
283
+ def extract_rows(body, response_key, result_key, collection_key)
284
+ result = body.dig(response_key, result_key)
285
+ return [] if result.nil?
286
+
287
+ rows = result[collection_key]
288
+ case rows
289
+ when nil then []
290
+ when Array then rows
291
+ else [rows]
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Kashflow
5
+ ##
6
+ # Rails engine for the KashFlow integration.
7
+ #
8
+ # Loads the gem's +app/+ tree into the host application and applies the
9
+ # gem's single decorator on each reload.
10
+ #
11
+ class Engine < ::Rails::Engine
12
+ require "spree/core"
13
+ isolate_namespace Spree
14
+
15
+ # Deliberately not "spree-kashflow": engine_name generates route helper
16
+ # prefixes and must be a valid Ruby identifier, so it cannot contain a dash.
17
+ engine_name "spree_kashflow"
18
+
19
+ config.generators do |g|
20
+ g.test_framework :rspec
21
+ end
22
+
23
+ ##
24
+ # Loads the gem's decorators. Called on every reload in development.
25
+ #
26
+ # @return [void]
27
+ #
28
+ def self.activate
29
+ # Three levels up from lib/spree/kashflow/ to reach the gem root.
30
+ Dir.glob(File.join(File.dirname(__FILE__), "../../../app/**/*_decorator*.rb")).sort.each do |c|
31
+ Rails.configuration.cache_classes ? require(c) : load(c)
32
+ end
33
+ end
34
+
35
+ config.to_prepare(&method(:activate).to_proc)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Kashflow
5
+ ##
6
+ # Base class for every error this gem raises.
7
+ #
8
+ class Error < StandardError; end
9
+
10
+ ##
11
+ # Raised when KashFlow rejects the configured credentials. Not retryable.
12
+ #
13
+ class AuthenticationError < Error; end
14
+
15
+ ##
16
+ # Raised when KashFlow accepts the request but refuses the operation.
17
+ #
18
+ class ApiError < Error; end
19
+
20
+ ##
21
+ # Raised when the KashFlow service could not be reached. Retryable.
22
+ #
23
+ class TransportError < Error; end
24
+
25
+ ##
26
+ # Raised by {Spree::Kashflow::InvoicePayload#to_h} when the assembled invoice
27
+ # lines fail to reconcile against either the line's own net total or the
28
+ # order's total. Nothing is posted to KashFlow when this is raised.
29
+ #
30
+ class TotalMismatchError < Error; end
31
+ end
32
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Kashflow
5
+ ##
6
+ # The Spree metafield keys ({Spree::Metafields#set_metafield} /
7
+ # {Spree::Metafields#get_metafield} "namespace.key" strings) this gem reads and
8
+ # writes. Centralised here so call sites never repeat the string literal, and so
9
+ # a later task (or the README) has one place to look up the whole table.
10
+ #
11
+ # `Spree::Metafields#set_metafield` auto-creates its backing
12
+ # `Spree::MetafieldDefinition` the first time a `"namespace.key"` string is used
13
+ # (`Spree::Metafields#resolve_metafield_definition_id_from_string`,
14
+ # spree_core-5.6.1 `app/models/concerns/spree/metafields.rb:222`) — confirmed
15
+ # empirically against the dummy app (see Task 7's report). No definition needs to
16
+ # be pre-seeded, but the auto-created definition takes `display_on`'s default of
17
+ # `"both"`, which would make a KashFlow invoice number — and raw KashFlow fault
18
+ # text in {ORDER_SYNC_ERROR} — storefront-visible by accident. Every write this
19
+ # gem makes therefore goes through {.write}, which seeds the definition as
20
+ # `"back_end"` first. These are internal accounting-sync markers; nothing here is
21
+ # ever meant for a customer.
22
+ #
23
+ module Metafields
24
+ # @return [String] order metafield; the KashFlow invoice number. Written
25
+ # immediately after `InsertInvoice_TypeDefined` returns, before any
26
+ # follow-on call, so a later failure can never cause the invoice to be
27
+ # posted twice. Its presence is the per-step guard {SyncOrderJob} uses to
28
+ # skip re-posting the invoice, and it is the identifier
29
+ # {CreditNotePayload} carries as `CustomerReference` when a refund is
30
+ # posted as a credit note.
31
+ ORDER_INVOICE_NUMBER = "kashflow.invoice_number"
32
+
33
+ # @return [String] order metafield; the timestamp at which the order's
34
+ # payment was recorded against the KashFlow invoice. Written immediately
35
+ # after `InsertInvoicePayment` succeeds, and checked separately from
36
+ # {ORDER_INVOICE_NUMBER} so a retry resumes at the payment step instead of
37
+ # re-posting the invoice or skipping the job wholesale.
38
+ ORDER_PAYMENT_RECORDED_AT = "kashflow.payment_recorded_at"
39
+
40
+ # @return [String] order metafield; the KashFlow customer id returned by
41
+ # `Client#upsert_customer`.
42
+ ORDER_CUSTOMER_CODE = "kashflow.customer_code"
43
+
44
+ # @return [String] order metafield; the timestamp of the last successful sync.
45
+ ORDER_SYNCED_AT = "kashflow.synced_at"
46
+
47
+ # @return [String] order metafield; the message of the last sync failure.
48
+ # Cleared (destroyed) on the next successful sync.
49
+ ORDER_SYNC_ERROR = "kashflow.sync_error"
50
+
51
+ # @return [String] refund metafield; the KashFlow credit note (invoice) number.
52
+ # Written immediately after the credit note is posted, before the link
53
+ # call, so a failing link can never cause a second credit note. Its
54
+ # presence is the per-step guard {SyncRefundJob} uses to skip re-posting.
55
+ REFUND_CREDIT_NOTE_NUMBER = "kashflow.credit_note_number"
56
+
57
+ # @return [String] refund metafield; the timestamp at which the credit note
58
+ # was linked to the original invoice via `applyCreditNoteToInvoice`.
59
+ # Checked separately from {REFUND_CREDIT_NOTE_NUMBER} so a retry re-runs
60
+ # only the link step. Re-applying a link is unverified against a live
61
+ # account, so this marker exists to avoid attempting it twice.
62
+ REFUND_CREDIT_NOTE_LINKED_AT = "kashflow.credit_note_linked_at"
63
+
64
+ # @return [String] the `display_on` value every definition this gem owns
65
+ # is seeded with: admin-only, never rendered on the storefront.
66
+ DISPLAY_ON = "back_end"
67
+
68
+ ##
69
+ # Writes one of this gem's metafields, seeding its backing
70
+ # `Spree::MetafieldDefinition` as {DISPLAY_ON} first so Spree's auto-create
71
+ # path never gets to default it to `"both"`.
72
+ #
73
+ # Idempotent and cheap: after the first write per key the seed is a single
74
+ # indexed lookup. Deliberately not a boot-time initializer or a migration —
75
+ # the gem ships neither, and a definition seeded at boot would need the
76
+ # database up before the app could load.
77
+ #
78
+ # @param record [#set_metafield] the {Spree::Order} or {Spree::Refund} to write to
79
+ # @param key [String] one of this module's `"namespace.key"` constants
80
+ # @param value [Object, nil] the value to store; `nil` clears the metafield
81
+ # @return [void]
82
+ #
83
+ # @example
84
+ # Spree::Kashflow::Metafields.write(order, Spree::Kashflow::Metafields::ORDER_INVOICE_NUMBER, 4471)
85
+ #
86
+ def self.write(record, key, value)
87
+ ensure_definition!(record, key)
88
+ record.set_metafield(key, value)
89
+ end
90
+
91
+ ##
92
+ # @param record [ActiveRecord::Base] the resource the definition belongs to
93
+ # @param key [String] a `"namespace.key"` string
94
+ # @return [Spree::MetafieldDefinition] the seeded (or already-present) definition
95
+ #
96
+ def self.ensure_definition!(record, key)
97
+ namespace, definition_key = key.split(".", 2)
98
+
99
+ Spree::MetafieldDefinition.find_or_create_by!(
100
+ namespace: namespace,
101
+ key: definition_key,
102
+ resource_type: record.class.name
103
+ ) { |definition| definition.display_on = DISPLAY_ON }
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Kashflow
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spree/core"
4
+ require "savon"
5
+ require "spree/kashflow/version"
6
+ require "spree/kashflow/errors"
7
+ require "spree/kashflow/metafields"
8
+ require "spree/kashflow/client"
9
+ require "spree/kashflow/engine"
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bundler auto-requires a gem by its *name*, so `gem "spree-kashflow"` in a host
4
+ # Gemfile issues `require "spree-kashflow"`. The real entry point is
5
+ # `spree/kashflow` (matching the Spree::Kashflow namespace), so this shim keeps
6
+ # the default `Bundler.require` working.
7
+ require "spree/kashflow"
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spree-kashflow
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Aypex
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: savon
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.17'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.17'
26
+ - !ruby/object:Gem::Dependency
27
+ name: spree
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 5.6.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 5.6.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: spree_extension
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: Pushes completed Spree orders to KashFlow as invoices and refunds as
55
+ credit notes over the KashFlow SOAP API, configured per store through Spree's Integrations
56
+ framework.
57
+ email: hello@aypex.io
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - app/assets/images/integration_icons/kashflow-logo.png
67
+ - app/jobs/spree/kashflow/sync_order_job.rb
68
+ - app/jobs/spree/kashflow/sync_refund_job.rb
69
+ - app/models/spree/integrations/kashflow.rb
70
+ - app/models/spree/refund_decorator.rb
71
+ - app/presenters/spree/kashflow/credit_note_payload.rb
72
+ - app/presenters/spree/kashflow/customer_payload.rb
73
+ - app/presenters/spree/kashflow/invoice_payload.rb
74
+ - app/subscribers/spree/kashflow/order_completed_subscriber.rb
75
+ - app/subscribers/spree/kashflow/reimbursement_subscriber.rb
76
+ - app/views/spree/admin/integrations/forms/_kashflow.html.erb
77
+ - config/initializers/spree.rb
78
+ - config/locales/en.yml
79
+ - config/routes.rb
80
+ - lib/spree-kashflow.rb
81
+ - lib/spree/kashflow.rb
82
+ - lib/spree/kashflow/client.rb
83
+ - lib/spree/kashflow/engine.rb
84
+ - lib/spree/kashflow/errors.rb
85
+ - lib/spree/kashflow/metafields.rb
86
+ - lib/spree/kashflow/version.rb
87
+ homepage: https://github.com/aypex-io/spree-kashflow
88
+ licenses:
89
+ - MIT
90
+ metadata:
91
+ source_code_uri: https://github.com/aypex-io/spree-kashflow
92
+ bug_tracker_uri: https://github.com/aypex-io/spree-kashflow/issues
93
+ changelog_uri: https://github.com/aypex-io/spree-kashflow/blob/main/CHANGELOG.md
94
+ rubygems_mfa_required: 'true'
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '3.3'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 4.0.16
110
+ specification_version: 4
111
+ summary: KashFlow accounting integration for Spree
112
+ test_files: []