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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE +22 -0
- data/README.md +257 -0
- data/Rakefile +21 -0
- data/app/assets/images/integration_icons/kashflow-logo.png +0 -0
- data/app/jobs/spree/kashflow/sync_order_job.rb +245 -0
- data/app/jobs/spree/kashflow/sync_refund_job.rb +169 -0
- data/app/models/spree/integrations/kashflow.rb +112 -0
- data/app/models/spree/refund_decorator.rb +31 -0
- data/app/presenters/spree/kashflow/credit_note_payload.rb +180 -0
- data/app/presenters/spree/kashflow/customer_payload.rb +138 -0
- data/app/presenters/spree/kashflow/invoice_payload.rb +286 -0
- data/app/subscribers/spree/kashflow/order_completed_subscriber.rb +24 -0
- data/app/subscribers/spree/kashflow/reimbursement_subscriber.rb +25 -0
- data/app/views/spree/admin/integrations/forms/_kashflow.html.erb +18 -0
- data/config/initializers/spree.rb +17 -0
- data/config/locales/en.yml +12 -0
- data/config/routes.rb +4 -0
- data/lib/spree/kashflow/client.rb +296 -0
- data/lib/spree/kashflow/engine.rb +38 -0
- data/lib/spree/kashflow/errors.rb +32 -0
- data/lib/spree/kashflow/metafields.rb +107 -0
- data/lib/spree/kashflow/version.rb +7 -0
- data/lib/spree/kashflow.rb +9 -0
- data/lib/spree-kashflow.rb +7 -0
- metadata +112 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module Kashflow
|
|
5
|
+
##
|
|
6
|
+
# Maps a Spree order onto the shape KashFlow's `Invoice` complex type expects.
|
|
7
|
+
#
|
|
8
|
+
# TKF prices are VAT-inclusive; KashFlow wants net (VAT-exclusive) per-unit rates.
|
|
9
|
+
# Because a per-unit `Rate` is rounded to 4 decimal places, `Rate * Quantity` is
|
|
10
|
+
# not guaranteed to reconcile back to the line's net total, or the assembled
|
|
11
|
+
# invoice back to the order's total. `#to_h` asserts both reconciliations before
|
|
12
|
+
# returning and raises {Spree::Kashflow::TotalMismatchError} rather than post a
|
|
13
|
+
# silently-wrong invoice. Pure object: reads the order and its associations,
|
|
14
|
+
# performs no network calls, and writes nothing back to Spree.
|
|
15
|
+
#
|
|
16
|
+
class InvoicePayload
|
|
17
|
+
# @return [String] the description used for the shipping line
|
|
18
|
+
SHIPPING_DESCRIPTION = "Shipping"
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @param order [Spree::Order] a completed order to post as an invoice
|
|
22
|
+
# @param integration [Spree::Integrations::Kashflow] the integration whose
|
|
23
|
+
# nominal codes decide which ledger accounts the lines post to
|
|
24
|
+
#
|
|
25
|
+
def initialize(order, integration:)
|
|
26
|
+
@order = order
|
|
27
|
+
@integration = integration
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# @return [Array<Hash{String => Object}>] the KashFlow `InvoiceLine` structures:
|
|
32
|
+
# one per Spree line item, plus one for shipping
|
|
33
|
+
# @note Unreconciled. Only {#to_h} runs the correctness guard; a caller that
|
|
34
|
+
# needs the guarantee that these lines actually add up to the order's total
|
|
35
|
+
# (Task 6's credit notes, for instance) must go through {#to_h}, not call
|
|
36
|
+
# this directly.
|
|
37
|
+
#
|
|
38
|
+
def lines
|
|
39
|
+
line_entries.map { |entry| entry[:line] }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Assembles the KashFlow `Invoice` structure, guarding the arithmetic before
|
|
44
|
+
# returning it.
|
|
45
|
+
#
|
|
46
|
+
# @return [Hash{String => Object}] a KashFlow `Invoice` structure
|
|
47
|
+
# @raise [Spree::Kashflow::TotalMismatchError] when a line's `Rate * Quantity`
|
|
48
|
+
# fails to reconcile against its net total, or the assembled invoice fails
|
|
49
|
+
# to reconcile against the order's total
|
|
50
|
+
# @note Known limitation: orders with exclusive tax (`additional_tax_total`,
|
|
51
|
+
# added on top of the price rather than included in it) are not reconciled
|
|
52
|
+
# by this arithmetic and will always raise here — refused rather than
|
|
53
|
+
# mis-booked, but not otherwise handled by this mapper.
|
|
54
|
+
#
|
|
55
|
+
def to_h
|
|
56
|
+
entries = line_entries
|
|
57
|
+
assert_totals_reconcile!(entries)
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
"CurrencyCode" => order.currency,
|
|
61
|
+
"Lines" => entries.map { |entry| entry[:line] },
|
|
62
|
+
"NetAmount" => entries.sum { |entry| entry[:net_total] }.round(2),
|
|
63
|
+
"VATAmount" => entries.sum { |entry| entry[:line]["VatAmount"] }
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# @return [Spree::Order]
|
|
70
|
+
attr_reader :order
|
|
71
|
+
|
|
72
|
+
# @return [Spree::Integrations::Kashflow]
|
|
73
|
+
attr_reader :integration
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Assigns each entry's `Sort` as a 1-based line index (line items first, then
|
|
77
|
+
# shipping). 1-based, not 0-based: `Sort` is an ordering column and 0 is also
|
|
78
|
+
# the natural "unset" sentinel in the same .NET model, so a first line of 0
|
|
79
|
+
# would be ambiguous between "first" and "not sorted". Of every field this
|
|
80
|
+
# class fills with a placeholder, `Sort` is the one with observable
|
|
81
|
+
# behaviour — it controls line ordering on the rendered KashFlow invoice —
|
|
82
|
+
# and should be confirmed against a real sandbox call before relying on it.
|
|
83
|
+
#
|
|
84
|
+
# @return [Array<Hash{Symbol => Object}>] one entry per line item and one for
|
|
85
|
+
# shipping, each carrying both the public `:line` hash and the unrounded
|
|
86
|
+
# `:net_total` the guard reconciles it against
|
|
87
|
+
#
|
|
88
|
+
def line_entries
|
|
89
|
+
entries = order.line_items.map { |line_item| line_item_entry(line_item) } +
|
|
90
|
+
order.shipments.map { |shipment| shipment_entry(shipment) }
|
|
91
|
+
entries.each_with_index { |entry, index| entry[:line]["Sort"] = index + 1 }
|
|
92
|
+
allocate_rounding_residue!(entries)
|
|
93
|
+
entries
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
##
|
|
97
|
+
# Largest-remainder penny allocation, run before the guard.
|
|
98
|
+
#
|
|
99
|
+
# `taxable_basis` returns an unrounded figure whenever a whole-order
|
|
100
|
+
# promotion applies: the discount is allocated across lines, so each
|
|
101
|
+
# line's share carries a fractional residue that only cancels when the
|
|
102
|
+
# whole set is summed. Three £10 lines with a £5 order-level discount give
|
|
103
|
+
# each line a basis of `8.333333…`, which rounds to `8.33` and assembles
|
|
104
|
+
# to `24.99` against an `order.total` of `25.00`. With two lines the
|
|
105
|
+
# residues happen to cancel, which is why this went unnoticed.
|
|
106
|
+
#
|
|
107
|
+
# Rather than loosen the guard — exact equality is the point of it — each
|
|
108
|
+
# entry's net total is rounded to the penny here and the leftover penny or
|
|
109
|
+
# two is pushed onto the largest line, the conventional allocation and the
|
|
110
|
+
# one where a penny is proportionally least visible.
|
|
111
|
+
#
|
|
112
|
+
# Only genuine rounding noise is absorbed: the residue is left alone (and
|
|
113
|
+
# the guard therefore still raises) once it exceeds one penny per line.
|
|
114
|
+
# That bound is what keeps the exclusive-tax case a refusal rather than a
|
|
115
|
+
# silent mis-booking — an order carrying `additional_tax_total` misses by
|
|
116
|
+
# the whole tax amount, not by pennies.
|
|
117
|
+
#
|
|
118
|
+
# @param entries [Array<Hash{Symbol => Object}>]
|
|
119
|
+
# @return [Array<Hash{Symbol => Object}>] the same entries, mutated in place
|
|
120
|
+
#
|
|
121
|
+
def allocate_rounding_residue!(entries)
|
|
122
|
+
return entries if entries.empty?
|
|
123
|
+
|
|
124
|
+
target = (order.total - entries.sum { |entry| entry[:line]["VatAmount"] }).round(2)
|
|
125
|
+
entries.each { |entry| entry[:net_total] = entry[:net_total].round(2) }
|
|
126
|
+
residue = target - entries.sum { |entry| entry[:net_total] }
|
|
127
|
+
|
|
128
|
+
if residue.nonzero? && residue.abs <= entries.length * BigDecimal("0.01")
|
|
129
|
+
entries.max_by { |entry| entry[:net_total].abs }[:net_total] += residue
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
entries.each { |entry| entry[:line]["Rate"] = (entry[:net_total] / entry[:line]["Quantity"]).round(4) }
|
|
133
|
+
entries
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
##
|
|
137
|
+
# @param line_item [Spree::LineItem]
|
|
138
|
+
# @return [Hash{Symbol => Object}]
|
|
139
|
+
#
|
|
140
|
+
def line_item_entry(line_item)
|
|
141
|
+
build_entry(
|
|
142
|
+
gross: line_item.taxable_basis,
|
|
143
|
+
included_tax_total: line_item.included_tax_total,
|
|
144
|
+
quantity: BigDecimal(line_item.quantity),
|
|
145
|
+
description: line_item_description(line_item),
|
|
146
|
+
charge_type: integration.preferred_sales_nominal_code
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
##
|
|
151
|
+
# @param shipment [Spree::Shipment]
|
|
152
|
+
# @return [Hash{Symbol => Object}]
|
|
153
|
+
#
|
|
154
|
+
def shipment_entry(shipment)
|
|
155
|
+
build_entry(
|
|
156
|
+
gross: shipment.taxable_basis,
|
|
157
|
+
included_tax_total: shipment.included_tax_total,
|
|
158
|
+
quantity: BigDecimal(1),
|
|
159
|
+
description: SHIPPING_DESCRIPTION,
|
|
160
|
+
charge_type: integration.preferred_shipping_nominal_code
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
##
|
|
165
|
+
# @param gross [BigDecimal] the taxable basis: the discounted, VAT-inclusive
|
|
166
|
+
# amount Spree itself taxes (`taxable_basis`), which already accounts for
|
|
167
|
+
# both line-level and whole-order promotion allocations
|
|
168
|
+
# @param included_tax_total [BigDecimal] the VAT baked into `gross`
|
|
169
|
+
# @param quantity [BigDecimal]
|
|
170
|
+
# @param description [String]
|
|
171
|
+
# @param charge_type [Integer] the KashFlow nominal code for this line
|
|
172
|
+
# @return [Hash{Symbol => Object}]
|
|
173
|
+
#
|
|
174
|
+
def build_entry(gross:, included_tax_total:, quantity:, description:, charge_type:)
|
|
175
|
+
net_total = gross - included_tax_total
|
|
176
|
+
rate = (net_total / quantity).round(4)
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
net_total: net_total,
|
|
180
|
+
line: {
|
|
181
|
+
"Quantity" => quantity,
|
|
182
|
+
"Description" => description,
|
|
183
|
+
"Rate" => rate,
|
|
184
|
+
"ChargeType" => charge_type,
|
|
185
|
+
"VatRate" => vat_rate(included_tax_total, net_total),
|
|
186
|
+
"VatAmount" => included_tax_total,
|
|
187
|
+
# KashFlow does not sync against the Spree catalogue, so there is no
|
|
188
|
+
# KashFlow product to reference.
|
|
189
|
+
"ProductID" => 0,
|
|
190
|
+
# Seeded here (not just appended later) to hold this key's position in
|
|
191
|
+
# WSDL sequence order — Savon serialises a Hash body in insertion
|
|
192
|
+
# order, and a .NET ASMX endpoint enforcing <s:sequence> will drop or
|
|
193
|
+
# mis-bind an out-of-order element rather than raise. #line_entries
|
|
194
|
+
# overwrites this in place once every entry's position is known.
|
|
195
|
+
"Sort" => nil,
|
|
196
|
+
# No KashFlow project is associated with these invoices.
|
|
197
|
+
"ProjID" => 0,
|
|
198
|
+
# Assigned by KashFlow on insert; not known until then.
|
|
199
|
+
"LineID" => 0
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
##
|
|
205
|
+
# @param included_tax_total [BigDecimal]
|
|
206
|
+
# @param net_total [BigDecimal]
|
|
207
|
+
# @return [BigDecimal] the VAT rate as a percentage, 0 when the line is
|
|
208
|
+
# entirely discounted away
|
|
209
|
+
#
|
|
210
|
+
def vat_rate(included_tax_total, net_total)
|
|
211
|
+
return BigDecimal(0) if net_total.zero?
|
|
212
|
+
(included_tax_total / net_total * 100).round(2)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
##
|
|
216
|
+
# @param line_item [Spree::LineItem]
|
|
217
|
+
# @return [String] the product name, suffixed with the applied promotion
|
|
218
|
+
# names when any eligible promotion adjustment applies to this line
|
|
219
|
+
#
|
|
220
|
+
def line_item_description(line_item)
|
|
221
|
+
names = promotion_names(line_item)
|
|
222
|
+
return line_item.name if names.empty?
|
|
223
|
+
"#{line_item.name} (#{names.join(", ")} applied)"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
##
|
|
227
|
+
# Uses `Spree::Promotion#name_for_order`, not `#code`. `code` is nilled out
|
|
228
|
+
# by a `before_validation` for *both* `multi_codes?` and `automatic?`
|
|
229
|
+
# promotions (spree_core `app/models/spree/promotion.rb:49`), so reading it
|
|
230
|
+
# directly drops the annotation entirely for automatic discounts — the
|
|
231
|
+
# common case. `name_for_order` returns the order's actual coupon code for
|
|
232
|
+
# a coupon promotion and the promotion's name otherwise.
|
|
233
|
+
#
|
|
234
|
+
# @param line_item [Spree::LineItem]
|
|
235
|
+
# @return [Array<String>] the names (or codes) of eligible promotions
|
|
236
|
+
# applied to this line
|
|
237
|
+
#
|
|
238
|
+
def promotion_names(line_item)
|
|
239
|
+
line_item.adjustments.select(&:promotion?).select(&:eligible?)
|
|
240
|
+
.map { |adjustment| adjustment.source.promotion.name_for_order(order).presence }
|
|
241
|
+
.compact.uniq
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
##
|
|
245
|
+
# The correctness guard. Raises rather than returns a payload whose figures
|
|
246
|
+
# do not reconcile — a failed sync is a queryable flag, a wrong invoice is a
|
|
247
|
+
# discrepancy someone finds at year end.
|
|
248
|
+
#
|
|
249
|
+
# Exact equality is deliberate and must stay that way. Penny residue from a
|
|
250
|
+
# whole-order promotion is dealt with upstream in
|
|
251
|
+
# {#allocate_rounding_residue!}, not by widening the comparison here.
|
|
252
|
+
#
|
|
253
|
+
# @note Known limitation: this guard only accounts for VAT baked into the
|
|
254
|
+
# price (`included_tax_total`) and the whole-order allocation captured by
|
|
255
|
+
# `taxable_basis`. An order with exclusive tax (`additional_tax_total`,
|
|
256
|
+
# added on top of the price rather than included in it) is not reconciled
|
|
257
|
+
# by this arithmetic and will make the guard raise — refused rather than
|
|
258
|
+
# mis-booked, which is the safe direction, but exclusive-tax orders are not
|
|
259
|
+
# otherwise handled by this mapper.
|
|
260
|
+
#
|
|
261
|
+
# @param entries [Array<Hash{Symbol => Object}>]
|
|
262
|
+
# @return [void]
|
|
263
|
+
# @raise [Spree::Kashflow::TotalMismatchError] when any line's `Rate * Quantity`
|
|
264
|
+
# fails to reconcile against its own net total, or the sum across all lines
|
|
265
|
+
# fails to reconcile against `order.total`
|
|
266
|
+
#
|
|
267
|
+
def assert_totals_reconcile!(entries)
|
|
268
|
+
entries.each do |entry|
|
|
269
|
+
reconciled = (entry[:line]["Rate"] * entry[:line]["Quantity"]).round(2)
|
|
270
|
+
expected = entry[:net_total].round(2)
|
|
271
|
+
next if reconciled == expected
|
|
272
|
+
|
|
273
|
+
raise TotalMismatchError,
|
|
274
|
+
"line #{entry[:line]["Description"].inspect}: Rate * Quantity = #{reconciled}, " \
|
|
275
|
+
"net_total = #{expected}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
assembled = entries.sum { |entry| (entry[:line]["Rate"] * entry[:line]["Quantity"]).round(2) + entry[:line]["VatAmount"] }
|
|
279
|
+
return if assembled == order.total
|
|
280
|
+
|
|
281
|
+
raise TotalMismatchError,
|
|
282
|
+
"assembled invoice total = #{assembled}, order.total = #{order.total}"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module Kashflow
|
|
5
|
+
##
|
|
6
|
+
# Enqueues {SyncOrderJob} whenever Spree publishes `order.completed`.
|
|
7
|
+
#
|
|
8
|
+
class OrderCompletedSubscriber < Spree::Subscriber
|
|
9
|
+
subscribes_to "order.completed"
|
|
10
|
+
|
|
11
|
+
##
|
|
12
|
+
# @param event [Spree::Event] payload carries the order's prefixed id
|
|
13
|
+
# under `"id"`
|
|
14
|
+
# @return [void]
|
|
15
|
+
#
|
|
16
|
+
def handle(event)
|
|
17
|
+
order = Spree::Order.find_by_prefix_id(event.payload["id"])
|
|
18
|
+
return if order.nil?
|
|
19
|
+
|
|
20
|
+
SyncOrderJob.perform_later(order.id)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module Kashflow
|
|
5
|
+
##
|
|
6
|
+
# Enqueues {SyncRefundJob} for each refund on a reimbursement whenever
|
|
7
|
+
# Spree publishes `reimbursement.reimbursed`.
|
|
8
|
+
#
|
|
9
|
+
class ReimbursementSubscriber < Spree::Subscriber
|
|
10
|
+
subscribes_to "reimbursement.reimbursed"
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# @param event [Spree::Event] payload carries the reimbursement's
|
|
14
|
+
# prefixed id under `"id"`
|
|
15
|
+
# @return [void]
|
|
16
|
+
#
|
|
17
|
+
def handle(event)
|
|
18
|
+
reimbursement = Spree::Reimbursement.find_by_prefix_id(event.payload["id"])
|
|
19
|
+
return if reimbursement.nil?
|
|
20
|
+
|
|
21
|
+
reimbursement.refunds.each { |refund| SyncRefundJob.perform_later(refund.id) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div class="row">
|
|
2
|
+
<div class="col-12">
|
|
3
|
+
<%= preference_field(@integration, form, 'username', i18n_scope: 'admin.integrations.kashflow') %>
|
|
4
|
+
<%= preference_field(@integration, form, 'password', i18n_scope: 'admin.integrations.kashflow') %>
|
|
5
|
+
<%= content_tag(:div, form.label("preferred_sales_nominal_code", Spree.t(:sales_nominal_code, scope: "admin.integrations.kashflow")) +
|
|
6
|
+
form.select("preferred_sales_nominal_code", options_for_select(@integration.nominal_code_options, @integration.preferred_sales_nominal_code)),
|
|
7
|
+
class: "form-group", id: "spree-integrations-kashflow-preference-sales_nominal_code") %>
|
|
8
|
+
<%= content_tag(:div, form.label("preferred_shipping_nominal_code", Spree.t(:shipping_nominal_code, scope: "admin.integrations.kashflow")) +
|
|
9
|
+
form.select("preferred_shipping_nominal_code", options_for_select(@integration.nominal_code_options, @integration.preferred_shipping_nominal_code)),
|
|
10
|
+
class: "form-group", id: "spree-integrations-kashflow-preference-shipping_nominal_code") %>
|
|
11
|
+
<%= content_tag(:div, form.label("preferred_bank_account_id", Spree.t(:bank_account_id, scope: "admin.integrations.kashflow")) +
|
|
12
|
+
form.select("preferred_bank_account_id", options_for_select(@integration.bank_account_options, @integration.preferred_bank_account_id)),
|
|
13
|
+
class: "form-group", id: "spree-integrations-kashflow-preference-bank_account_id") %>
|
|
14
|
+
<%= content_tag(:div, form.label("preferred_payment_method_id", Spree.t(:payment_method_id, scope: "admin.integrations.kashflow")) +
|
|
15
|
+
form.select("preferred_payment_method_id", options_for_select(@integration.payment_method_options, @integration.preferred_payment_method_id)),
|
|
16
|
+
class: "form-group", id: "spree-integrations-kashflow-preference-payment_method_id") %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The after_initialize wrapper is load-bearing: spree_core ASSIGNS
|
|
4
|
+
# config.spree.integrations = [] inside its own after_initialize, which runs
|
|
5
|
+
# AFTER engine initializers and after config/initializers files. Registering at
|
|
6
|
+
# file scope here would be silently clobbered and the integration would never
|
|
7
|
+
# appear in the admin. spec/spree/kashflow/registration_spec.rb pins this.
|
|
8
|
+
#
|
|
9
|
+
# The two subscriber registrations live in this same block for consistency
|
|
10
|
+
# with that rule, even though Spree.subscribers itself is only ever
|
|
11
|
+
# concatenated onto (never reassigned) after it is first set.
|
|
12
|
+
Rails.application.config.after_initialize do
|
|
13
|
+
Rails.application.config.spree.integrations << Spree::Integrations::Kashflow
|
|
14
|
+
|
|
15
|
+
Spree.subscribers << Spree::Kashflow::OrderCompletedSubscriber
|
|
16
|
+
Spree.subscribers << Spree::Kashflow::ReimbursementSubscriber
|
|
17
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
en:
|
|
2
|
+
spree:
|
|
3
|
+
admin:
|
|
4
|
+
integrations:
|
|
5
|
+
kashflow:
|
|
6
|
+
brand_name: "KashFlow"
|
|
7
|
+
username: "API username"
|
|
8
|
+
password: "API password"
|
|
9
|
+
sales_nominal_code: "Sales nominal code"
|
|
10
|
+
shipping_nominal_code: "Shipping nominal code"
|
|
11
|
+
bank_account_id: "Bank account"
|
|
12
|
+
payment_method_id: "Payment method"
|
data/config/routes.rb
ADDED