spree-kashflow 0.1.0 → 0.1.2
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 +4 -4
- data/CHANGELOG.md +35 -0
- data/app/assets/images/integration_icons/kashflow-logo.png +0 -0
- data/app/presenters/spree/kashflow/customer_payload.rb +72 -1
- data/lib/spree/kashflow/client.rb +44 -1
- data/lib/spree/kashflow/version.rb +1 -1
- data/lib/spree/kashflow.rb +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d20212552f51bc299bb20645fc8f5dbca833b2bdb63a1678d752b29a15f00a17
|
|
4
|
+
data.tar.gz: fb65959bc02ff1ac1b1eb7b38257ca03b9fba95386efb49b40baf91378b7c0bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a257c8a10a8576b39a5b58613568cc1faf751f3fa28caa505df9d190404be40d12e0e52272a48c3f2f141cc7e2b0c540b59bf32bb2501d7c2dd60b01b31ecf9b
|
|
7
|
+
data.tar.gz: 19f304c825178155659864ba3b064507ade831b27632d385befd8d27a60f84ccd74471b0eef8ce94cea0f1bd1391ce900fc034af404377e578d87e292527a276
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
- **Fix: every order sync still failed against a live KashFlow account**, now on
|
|
6
|
+
the invoice rather than the customer. KashFlow types every date field in the
|
|
7
|
+
WSDL as `s:dateTime`, and its .NET `XmlSerializer` rejected the whole envelope
|
|
8
|
+
with `The string '2026-08-18 10:53:48 UTC' is not a valid AllXsd value` —
|
|
9
|
+
no invoice created, and the fault naming only a character offset. Gyoku
|
|
10
|
+
type-switches with `Module#===`, so `ActiveSupport::TimeWithZone` (a
|
|
11
|
+
delegator, not a `Time` subclass) fell through to `to_s`; plain `Time` did
|
|
12
|
+
too, and `Date` emitted an xsd `date` rather than a `dateTime`. Since
|
|
13
|
+
`Time.current` and Active Record datetime attributes all return
|
|
14
|
+
`TimeWithZone`, this was the default path, affecting `InvoiceDate`, `DueDate`
|
|
15
|
+
and `PayDate` on orders and `InvoiceDate` / `DueDate` on refunds.
|
|
16
|
+
`Client#call` now normalises every temporal value in an outgoing message to
|
|
17
|
+
UTC xsd `dateTime`, so the wire format is owned by the one class that touches
|
|
18
|
+
the wire and any date field added later is correct by default.
|
|
19
|
+
- Assert the *serialised* SOAP body for temporal values. The suite doubled the
|
|
20
|
+
client and asserted the payload Hash, where a `TimeWithZone` looks correct
|
|
21
|
+
right up until it reaches the wire — which is why 0.1.1 shipped with this
|
|
22
|
+
defect behind a green build.
|
|
23
|
+
|
|
24
|
+
## 0.1.1
|
|
25
|
+
|
|
26
|
+
- **Fix: every order sync failed against a live KashFlow account.** The customer
|
|
27
|
+
`Code` was set to the order's email address, which KashFlow rejects — "the
|
|
28
|
+
customer code specified is invalid, please re-enter without any special
|
|
29
|
+
characters" — on the customer upsert, before an invoice was ever attempted.
|
|
30
|
+
Codes are now uppercase-alphanumeric and derived from the customer rather than
|
|
31
|
+
the order: `SPU<user id>` for a registered user, `SPG<email digest>` for a
|
|
32
|
+
guest. Both are stable across that customer's orders, which is what keeps
|
|
33
|
+
`InsertCustomer` an update rather than a duplicate.
|
|
34
|
+
- Require `digest` explicitly rather than relying on a transitive dependency to
|
|
35
|
+
load it.
|
|
36
|
+
- Replace the placeholder integration logo with the IRIS KashFlow brand asset.
|
|
37
|
+
|
|
3
38
|
## 0.1.0
|
|
4
39
|
|
|
5
40
|
- Initial release.
|
|
Binary file
|
|
@@ -23,6 +23,42 @@ module Spree
|
|
|
23
23
|
# column, so this gem reads it out of `order.metadata` under this key.
|
|
24
24
|
VAT_NUMBER_METADATA_KEY = "vat_number"
|
|
25
25
|
|
|
26
|
+
##
|
|
27
|
+
# KashFlow rejects a customer `Code` that carries special characters:
|
|
28
|
+
#
|
|
29
|
+
# NO: The customer code specified is invalid, please re-enter without
|
|
30
|
+
# any special characters.
|
|
31
|
+
#
|
|
32
|
+
# v0.1.0 sent `order.email` here, so EVERY sync failed on the customer
|
|
33
|
+
# upsert before an invoice was ever attempted — caught on the first live
|
|
34
|
+
# order (tkf-prd order 12, 2026-08-18). The WSDL types `Code` as a bare
|
|
35
|
+
# `s:string` with no facets, so the rule is server-side and undiscoverable
|
|
36
|
+
# from the schema; these codes are therefore deliberately conservative —
|
|
37
|
+
# uppercase alphanumerics only, comfortably short.
|
|
38
|
+
#
|
|
39
|
+
# `InsertCustomer` upserts BY `Code`, so the code identifies the CUSTOMER,
|
|
40
|
+
# not the order. Anything per-order would create a fresh KashFlow customer
|
|
41
|
+
# on every purchase instead of updating the existing one.
|
|
42
|
+
|
|
43
|
+
# @return [String] format for a registered user's code, keyed on the Spree
|
|
44
|
+
# user id so it survives the customer changing their email address.
|
|
45
|
+
USER_CODE_FORMAT = "SPU%d"
|
|
46
|
+
|
|
47
|
+
# @return [String] format for a guest's code. Guests have no stable id, so
|
|
48
|
+
# the normalised email is hashed — same email in, same code out, which is
|
|
49
|
+
# what keeps the upsert an update rather than a duplicate.
|
|
50
|
+
GUEST_CODE_FORMAT = "SPG%s"
|
|
51
|
+
|
|
52
|
+
# @return [Integer] hex characters of the email digest kept in a guest code.
|
|
53
|
+
# 12 hex chars is 48 bits: collision-safe at any plausible customer count
|
|
54
|
+
# while leaving the total code short.
|
|
55
|
+
GUEST_CODE_DIGEST_LENGTH = 12
|
|
56
|
+
|
|
57
|
+
# @return [Integer] the longest code this presenter will emit. KashFlow does
|
|
58
|
+
# not publish the limit in the WSDL; this is a conservative ceiling that
|
|
59
|
+
# both formats stay well inside.
|
|
60
|
+
CODE_MAX_LENGTH = 20
|
|
61
|
+
|
|
26
62
|
##
|
|
27
63
|
# @param order [Spree::Order] a completed order with a billing address
|
|
28
64
|
#
|
|
@@ -43,7 +79,7 @@ module Spree
|
|
|
43
79
|
#
|
|
44
80
|
def to_h
|
|
45
81
|
payload = {
|
|
46
|
-
"Code" =>
|
|
82
|
+
"Code" => customer_code,
|
|
47
83
|
"Name" => customer_name,
|
|
48
84
|
"Email" => order.email,
|
|
49
85
|
"Address1" => bill_address&.address1,
|
|
@@ -66,6 +102,41 @@ module Spree
|
|
|
66
102
|
# @return [Spree::Order]
|
|
67
103
|
attr_reader :order
|
|
68
104
|
|
|
105
|
+
##
|
|
106
|
+
# The KashFlow customer reference. See {USER_CODE_FORMAT} for why this is
|
|
107
|
+
# keyed on the customer rather than the order, and why it is alphanumeric.
|
|
108
|
+
#
|
|
109
|
+
# The order-number fallback is unreachable for a completed order — Spree
|
|
110
|
+
# validates email presence — but it exists so a blank email cannot collapse
|
|
111
|
+
# every such order onto one shared code, which would merge unrelated
|
|
112
|
+
# customers into a single KashFlow record.
|
|
113
|
+
#
|
|
114
|
+
# @return [String] an uppercase-alphanumeric customer code
|
|
115
|
+
#
|
|
116
|
+
def customer_code
|
|
117
|
+
return format(USER_CODE_FORMAT, order.user_id) if order.user_id.present?
|
|
118
|
+
return order.number if normalized_email.blank?
|
|
119
|
+
|
|
120
|
+
format(GUEST_CODE_FORMAT, email_digest)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
##
|
|
124
|
+
# @return [String] the order email, lowercased and stripped, so that the
|
|
125
|
+
# same guest is not split across several KashFlow customers by casing or
|
|
126
|
+
# stray whitespace
|
|
127
|
+
#
|
|
128
|
+
def normalized_email
|
|
129
|
+
order.email.to_s.strip.downcase
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
##
|
|
133
|
+
# @return [String] the leading {GUEST_CODE_DIGEST_LENGTH} hex characters of
|
|
134
|
+
# the normalised email's SHA-256, uppercased
|
|
135
|
+
#
|
|
136
|
+
def email_digest
|
|
137
|
+
Digest::SHA256.hexdigest(normalized_email)[0, GUEST_CODE_DIGEST_LENGTH].upcase
|
|
138
|
+
end
|
|
139
|
+
|
|
69
140
|
##
|
|
70
141
|
# Nullable on purpose. A digital-only order can complete with no billing
|
|
71
142
|
# address at all, and a `NoMethodError` here would escape the sync job's
|
|
@@ -182,7 +182,8 @@ module Spree
|
|
|
182
182
|
#
|
|
183
183
|
def call(operation, message = {})
|
|
184
184
|
credentials = {"UserName" => @username, "Password" => @password}
|
|
185
|
-
|
|
185
|
+
body = coerce_temporal(credentials.merge(message))
|
|
186
|
+
response = savon_client.call(operation, message: body)
|
|
186
187
|
body = response.body
|
|
187
188
|
assert_status!(body)
|
|
188
189
|
body
|
|
@@ -195,6 +196,48 @@ module Spree
|
|
|
195
196
|
raise TransportError, e.message
|
|
196
197
|
end
|
|
197
198
|
|
|
199
|
+
##
|
|
200
|
+
# Rewrites every temporal value in an outgoing message to xsd `dateTime`.
|
|
201
|
+
#
|
|
202
|
+
# KashFlow types every date field in the WSDL as `s:dateTime`, and its
|
|
203
|
+
# .NET `XmlSerializer` rejects the *entire* envelope with a
|
|
204
|
+
# `FormatException` ("is not a valid AllXsd value") when one of them is
|
|
205
|
+
# not valid xsd — the invoice is never created, and the fault names only
|
|
206
|
+
# a character offset, not the field.
|
|
207
|
+
#
|
|
208
|
+
# Gyoku cannot be relied on to do this. It type-switches with
|
|
209
|
+
# `case/when`, i.e. `Module#===`, so:
|
|
210
|
+
# - `ActiveSupport::TimeWithZone` is a *delegator*, not a `Time`
|
|
211
|
+
# subclass, and misses the branch entirely -> `to_s`
|
|
212
|
+
# ("2026-08-18 10:53:48 UTC").
|
|
213
|
+
# - plain `Time` is likewise emitted via `to_s` by Gyoku 1.4.
|
|
214
|
+
# - `Date` serialises as `2026-08-18`, an xsd `date`, not the `dateTime`
|
|
215
|
+
# the schema asks for.
|
|
216
|
+
#
|
|
217
|
+
# `Time.current` and every Active Record datetime attribute return
|
|
218
|
+
# `TimeWithZone`, so the broken path was the default one, which is why
|
|
219
|
+
# every caller was affected. Normalising here rather than at each call
|
|
220
|
+
# site keeps the wire format owned by the one class that touches the
|
|
221
|
+
# wire, and means a date field added to any future payload is correct
|
|
222
|
+
# without the author having to know this.
|
|
223
|
+
#
|
|
224
|
+
# Values are converted to UTC first so the instant is preserved and the
|
|
225
|
+
# emitted form is unambiguous (`...Z`) regardless of the app's zone.
|
|
226
|
+
#
|
|
227
|
+
# @param value [Object] any message value; Hashes and Arrays are walked
|
|
228
|
+
# @return [Object] the value with temporals replaced by xsd strings
|
|
229
|
+
#
|
|
230
|
+
def coerce_temporal(value)
|
|
231
|
+
case value
|
|
232
|
+
when Hash then value.transform_values { |element| coerce_temporal(element) }
|
|
233
|
+
when Array then value.map { |element| coerce_temporal(element) }
|
|
234
|
+
when ActiveSupport::TimeWithZone, Time then value.utc.xmlschema
|
|
235
|
+
when DateTime then value.to_time.utc.xmlschema
|
|
236
|
+
when Date then value.to_time(:utc).xmlschema
|
|
237
|
+
else value
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
198
241
|
##
|
|
199
242
|
# KashFlow reports business-level rejections *in band*: the SOAP call
|
|
200
243
|
# succeeds at HTTP 200 with an empty result element and a `Status` /
|
data/lib/spree/kashflow.rb
CHANGED