swoosh 0.2.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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +51 -0
  3. data/.tool-versions +1 -0
  4. data/AGENTS.md +13 -0
  5. data/CHANGELOG.md +68 -0
  6. data/Gemfile +17 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +377 -0
  9. data/Rakefile +32 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/certs/Swish_Merchant_TestCertificate_1234679304.csr +27 -0
  13. data/certs/Swish_Merchant_TestCertificate_1234679304.key +52 -0
  14. data/certs/Swish_Merchant_TestCertificate_1234679304.p12 +0 -0
  15. data/certs/Swish_Merchant_TestCertificate_1234679304.pem +98 -0
  16. data/certs/Swish_Merchant_TestSigningCertificate_1234679304.csr +27 -0
  17. data/certs/Swish_Merchant_TestSigningCertificate_1234679304.key +52 -0
  18. data/certs/Swish_Merchant_TestSigningCertificate_1234679304.p12 +0 -0
  19. data/certs/Swish_Merchant_TestSigningCertificate_1234679304.pem +98 -0
  20. data/certs/Swish_TLS_RootCA.pem +22 -0
  21. data/certs/Swish_TechnicalSupplier_TestCertificate_9870474641.csr +27 -0
  22. data/certs/Swish_TechnicalSupplier_TestCertificate_9870474641.key +52 -0
  23. data/certs/Swish_TechnicalSupplier_TestCertificate_9870474641.p12 +0 -0
  24. data/certs/Swish_TechnicalSupplier_TestCertificate_9870474641.pem +98 -0
  25. data/lib/swoosh/callback/controller.rb +31 -0
  26. data/lib/swoosh/callback.rb +35 -0
  27. data/lib/swoosh/certificates.rb +92 -0
  28. data/lib/swoosh/configuration.rb +54 -0
  29. data/lib/swoosh/errors.rb +100 -0
  30. data/lib/swoosh/payment.rb +101 -0
  31. data/lib/swoosh/payment_request.rb +53 -0
  32. data/lib/swoosh/qr_code.rb +36 -0
  33. data/lib/swoosh/railtie.rb +41 -0
  34. data/lib/swoosh/test.rb +116 -0
  35. data/lib/swoosh/token_store.rb +66 -0
  36. data/lib/swoosh/version.rb +5 -0
  37. data/lib/swoosh.rb +172 -0
  38. metadata +86 -0
data/lib/swoosh.rb ADDED
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "openssl"
6
+ require "securerandom"
7
+ require "uri"
8
+ require_relative "swoosh/version"
9
+ require_relative "swoosh/errors"
10
+ require_relative "swoosh/configuration"
11
+ require_relative "swoosh/certificates"
12
+ require_relative "swoosh/payment"
13
+ require_relative "swoosh/payment_request"
14
+ require_relative "swoosh/token_store"
15
+ require_relative "swoosh/qr_code"
16
+ require_relative "swoosh/callback"
17
+ require_relative "swoosh/railtie" if defined? Rails::Railtie
18
+
19
+ module Swoosh
20
+ class << self
21
+ attr_writer :client
22
+
23
+ def configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ # Swoosh.configure { |c| c.environment = :production }
28
+ def configure
29
+ yield configuration
30
+ @client = nil
31
+ configuration
32
+ end
33
+
34
+ def client
35
+ @client ||= Main.new(configuration: configuration)
36
+ end
37
+
38
+ def reset!
39
+ @configuration = nil
40
+ @client = nil
41
+ end
42
+
43
+ def generate_payment(amount, **options)
44
+ client.generate_payment(amount, **options)
45
+ end
46
+
47
+ def find_payment(id)
48
+ client.find_payment(id)
49
+ end
50
+
51
+ def cancel_payment(id)
52
+ client.cancel_payment(id)
53
+ end
54
+
55
+ def token_for(payment_id)
56
+ client.token_for(payment_id)
57
+ end
58
+ end
59
+
60
+ class Main
61
+ TEST_URL = "https://mss.cpc.getswish.net/swish-cpcapi/api/v2/paymentrequests"
62
+ PRODUCTION_URL = "https://cpc.getswish.net/swish-cpcapi/api/v2/paymentrequests"
63
+
64
+ # Creating a payment is v2 (we supply the id); reading one back is v1.
65
+ TEST_LOOKUP_URL = "https://mss.cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests"
66
+ PRODUCTION_LOOKUP_URL = "https://cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests"
67
+
68
+ JSON_CONTENT_TYPE = "application/json"
69
+
70
+ # Cancelling is a JSON Patch against the payment's status, not a body of its
71
+ # own, and Swish rejects it with a 415 under any other content type.
72
+ JSON_PATCH_CONTENT_TYPE = "application/json-patch+json"
73
+ CANCEL_PATCH = [{ op: "replace", path: "/status", value: "cancelled" }].freeze
74
+
75
+ REQUEST_CLASSES = {
76
+ get: Net::HTTP::Get,
77
+ put: Net::HTTP::Put,
78
+ patch: Net::HTTP::Patch
79
+ }.freeze
80
+
81
+ attr_reader :configuration, :certificates, :token_store
82
+
83
+ def initialize(configuration: Swoosh.configuration)
84
+ @configuration = configuration
85
+ @certificates = Certificates.new(configuration)
86
+ @token_store = TokenStore.new(configuration.token_store, ttl: configuration.token_ttl)
87
+ end
88
+
89
+ def environment = configuration.environment
90
+ def url = configuration.production? ? PRODUCTION_URL : TEST_URL
91
+ def lookup_url = configuration.production? ? PRODUCTION_LOOKUP_URL : TEST_LOOKUP_URL
92
+
93
+ # Swoosh.generate_payment(100, callback_url: "https://example.com/swish")
94
+ #
95
+ # Omit payer_alias for the Swish-app (m-commerce) flow and the payment comes
96
+ # back carrying a token; supply one and Swish notifies that number instead.
97
+ def generate_payment(amount, **options)
98
+ id = uuid
99
+ response = request(:put, "#{url}/#{id}", body: JSON.generate(data(amount, **options)))
100
+ # Net::HTTP matches header names case-insensitively, which matters: Swish
101
+ # sends this back as "Paymentrequesttoken".
102
+ token = response["PaymentRequestToken"]
103
+ token_store.write(id, token)
104
+
105
+ Payment.new({ "id" => id, "status" => Payment::CREATED }, token: token)
106
+ end
107
+
108
+ def find_payment(id)
109
+ Payment.from_json(request(:get, "#{lookup_url}/#{id}").body.to_s)
110
+ end
111
+
112
+ # Withdraws a payment request the payer hasn't answered yet, so an abandoned
113
+ # checkout stops waiting out the full three minutes.
114
+ #
115
+ # Only a CREATED payment can be cancelled, so expect the race: the payer
116
+ # accepts between your decision to cancel and this request arriving. Swish
117
+ # reports that as PaymentNotCancellable and a second cancel as
118
+ # PaymentAlreadyCancelled -- believe find_payment over your own assumption
119
+ # about which state the payment was in.
120
+ def cancel_payment(id)
121
+ response = request(
122
+ :patch, "#{lookup_url}/#{id}",
123
+ body: JSON.generate(CANCEL_PATCH),
124
+ content_type: JSON_PATCH_CONTENT_TYPE
125
+ )
126
+ # The token dies with the request; keeping it would let token_for hand back
127
+ # something that still renders a QR nobody can pay.
128
+ token_store.delete(id)
129
+
130
+ Payment.from_json(response.body.to_s)
131
+ end
132
+
133
+ # The token stored when the payment was created, or nil. Nil means "create a
134
+ # fresh payment request" -- never an error.
135
+ def token_for(payment_id) = token_store.read(payment_id)
136
+
137
+ def data(amount, **options)
138
+ PaymentRequest.new(configuration: configuration, amount: amount, **options).to_h
139
+ end
140
+
141
+ def uuid = SecureRandom.uuid.delete("-").upcase
142
+
143
+ def cert = certificates.cert
144
+ def root_cert = certificates.root_ca
145
+
146
+ # A connection carrying the merchant certificate, ready to issue one
147
+ # request. Net::HTTP opens and closes it around #request on its own.
148
+ def connection(uri)
149
+ certificates.configure_ssl(Net::HTTP.new(uri.host, uri.port))
150
+ end
151
+
152
+ private
153
+
154
+ def request(verb, target, body: nil, content_type: JSON_CONTENT_TYPE)
155
+ uri = URI.parse(target)
156
+ response = connection(uri).request(build_request(verb, uri, body, content_type))
157
+ return response if response.is_a?(Net::HTTPSuccess)
158
+
159
+ raise ResponseError.for(status: response.code.to_i, body: response.body.to_s)
160
+ end
161
+
162
+ def build_request(verb, uri, body, content_type)
163
+ REQUEST_CLASSES.fetch(verb).new(uri).tap do |request|
164
+ request["Accept"] = JSON_CONTENT_TYPE
165
+ next if body.nil?
166
+
167
+ request["Content-Type"] = content_type
168
+ request.body = body
169
+ end
170
+ end
171
+ end
172
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: swoosh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Johan Halse
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: 'Swoosh drives the merchant API behind Swish, the Swedish mobile payment
13
+ service: create a payment request, poll it, cancel it, and verify the callback Swish
14
+ posts when it settles. It resolves the mutual-TLS certificate your bank issues and
15
+ verifies Swish in return, and carries the m-commerce token that app-switch URLs
16
+ and QR codes are built from. Rails applications get a railtie and a controller concern;
17
+ plain Ruby, Sinatra and Hanami use the same API without them. No runtime dependencies.'
18
+ email:
19
+ - johan@hal.se
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - ".rubocop.yml"
25
+ - ".tool-versions"
26
+ - AGENTS.md
27
+ - CHANGELOG.md
28
+ - Gemfile
29
+ - LICENSE.txt
30
+ - README.md
31
+ - Rakefile
32
+ - bin/console
33
+ - bin/setup
34
+ - certs/Swish_Merchant_TestCertificate_1234679304.csr
35
+ - certs/Swish_Merchant_TestCertificate_1234679304.key
36
+ - certs/Swish_Merchant_TestCertificate_1234679304.p12
37
+ - certs/Swish_Merchant_TestCertificate_1234679304.pem
38
+ - certs/Swish_Merchant_TestSigningCertificate_1234679304.csr
39
+ - certs/Swish_Merchant_TestSigningCertificate_1234679304.key
40
+ - certs/Swish_Merchant_TestSigningCertificate_1234679304.p12
41
+ - certs/Swish_Merchant_TestSigningCertificate_1234679304.pem
42
+ - certs/Swish_TLS_RootCA.pem
43
+ - certs/Swish_TechnicalSupplier_TestCertificate_9870474641.csr
44
+ - certs/Swish_TechnicalSupplier_TestCertificate_9870474641.key
45
+ - certs/Swish_TechnicalSupplier_TestCertificate_9870474641.p12
46
+ - certs/Swish_TechnicalSupplier_TestCertificate_9870474641.pem
47
+ - lib/swoosh.rb
48
+ - lib/swoosh/callback.rb
49
+ - lib/swoosh/callback/controller.rb
50
+ - lib/swoosh/certificates.rb
51
+ - lib/swoosh/configuration.rb
52
+ - lib/swoosh/errors.rb
53
+ - lib/swoosh/payment.rb
54
+ - lib/swoosh/payment_request.rb
55
+ - lib/swoosh/qr_code.rb
56
+ - lib/swoosh/railtie.rb
57
+ - lib/swoosh/test.rb
58
+ - lib/swoosh/token_store.rb
59
+ - lib/swoosh/version.rb
60
+ homepage: https://github.com/johanhalse/swoosh
61
+ licenses:
62
+ - MIT
63
+ metadata:
64
+ allowed_push_host: https://rubygems.org
65
+ rubygems_mfa_required: 'true'
66
+ homepage_uri: https://github.com/johanhalse/swoosh
67
+ source_code_uri: https://github.com/johanhalse/swoosh
68
+ changelog_uri: https://github.com/johanhalse/swoosh/CHANGELOG.md
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 3.2.0
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 4.0.20
84
+ specification_version: 4
85
+ summary: Swish payments for Ruby, with first-class Rails support
86
+ test_files: []