ls-grid 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a70fe359eedafb694fea932b0caf61d78164a2f5b787501ea14b9b541bb8d281
4
+ data.tar.gz: d1c08c405350c5ffa25b7f1b13bad454a1eb7575a9eb02693e4586eda383bd7d
5
+ SHA512:
6
+ metadata.gz: 338b20fe1fdfb5d2410d4f422d123217ed443f603549911987ae7cc1ee192e32df3046ae947dc69810699e2fa3a2c9ad7e9bf3c17613fc6b6320f8989f76680f
7
+ data.tar.gz: 4412671c450972068867acb02015b6d8a74378c0cf4d05b9eaadba8b77d25ec86b64ccebce972df74efb27498a9f462417641018147fd9afa71423c47981af96
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-11-19
4
+
5
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Kang-Kyu Lee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,288 @@
1
+ # Grid API Ruby Client
2
+
3
+ A Ruby client library for the [Lightspark Grid API](https://github.com/lightsparkdev/grid-api), enabling modern financial institutions to easily send and receive global payments.
4
+
5
+ ## Features
6
+
7
+ - HTTP Basic Authentication
8
+ - Support for all Grid API endpoints
9
+ - Uses only Ruby's standard `net/http` library (no external HTTP dependencies)
10
+ - Support for fiat, stablecoins, and Bitcoin transactions
11
+ - Real-time settlement capabilities
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'ls-grid'
19
+ ```
20
+
21
+ Or install it directly:
22
+
23
+ ```bash
24
+ gem install ls-grid
25
+ ```
26
+
27
+ ## Configuration
28
+
29
+ Configure the Grid client with your API credentials:
30
+
31
+ ```ruby
32
+ require 'ls/grid'
33
+
34
+ Lightspark::Grid.configure do |config|
35
+ config.api_token_id = 'your_api_token_id'
36
+ config.api_client_secret = 'your_api_client_secret'
37
+ # Optional: override base URL (defaults to production)
38
+ # config.base_url = 'https://api.lightspark.com/grid/2025-10-13'
39
+ end
40
+
41
+ # Access the configured client
42
+ client = Lightspark::Grid.client
43
+ ```
44
+
45
+ Alternatively, create a client instance directly:
46
+
47
+ ```ruby
48
+ client = Lightspark::Grid::Client.new(
49
+ api_token_id: 'your_api_token_id',
50
+ api_client_secret: 'your_api_client_secret'
51
+ )
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ### Platform Configuration
57
+
58
+ ```ruby
59
+ # Get platform configuration
60
+ config = client.config.get
61
+
62
+ # Update platform configuration
63
+ client.config.update({
64
+ webhookUrl: 'https://your-domain.com/webhooks'
65
+ })
66
+ ```
67
+
68
+ ### Customer Management
69
+
70
+ ```ruby
71
+ # Create a customer
72
+ customer = client.customers.create({
73
+ type: 'INDIVIDUAL',
74
+ firstName: 'John',
75
+ lastName: 'Doe',
76
+ email: 'john@example.com'
77
+ })
78
+
79
+ # List customers
80
+ customers = client.customers.list(limit: 20)
81
+
82
+ # Get a specific customer
83
+ customer = client.customers.get('customer_id')
84
+
85
+ # Update a customer
86
+ client.customers.update('customer_id', {
87
+ email: 'newemail@example.com'
88
+ })
89
+
90
+ # Delete a customer
91
+ client.customers.delete('customer_id')
92
+
93
+ # Generate KYC link
94
+ kyc = client.customers.kyc_link(
95
+ customer_id: 'customer_id',
96
+ redirect_url: 'https://your-app.com/kyc-complete'
97
+ )
98
+
99
+ # Bulk create customers via CSV
100
+ job = client.customers.bulk_create_csv(csv_content)
101
+
102
+ # Check bulk import job status
103
+ status = client.customers.bulk_job_status(job['id'])
104
+ ```
105
+
106
+ ### Account Management
107
+
108
+ ```ruby
109
+ # List customer internal accounts
110
+ accounts = client.accounts.list_customer_internal_accounts(
111
+ customer_id: 'customer_id'
112
+ )
113
+
114
+ # List platform internal accounts
115
+ accounts = client.accounts.list_platform_internal_accounts
116
+
117
+ # List customer external accounts
118
+ accounts = client.accounts.list_customer_external_accounts(
119
+ customer_id: 'customer_id'
120
+ )
121
+
122
+ # Create customer external account
123
+ account = client.accounts.create_customer_external_account({
124
+ customerId: 'customer_id',
125
+ accountNumber: '123456789',
126
+ routingNumber: '987654321'
127
+ })
128
+
129
+ # Plaid integration
130
+ link_token = client.accounts.create_plaid_link_token(
131
+ customer_id: 'customer_id'
132
+ )
133
+
134
+ # Submit Plaid authentication
135
+ client.accounts.plaid_callback(
136
+ 'plaid_link_token',
137
+ public_token: 'plaid_public_token'
138
+ )
139
+ ```
140
+
141
+ ### Cross-Currency Transfers (Quotes)
142
+
143
+ ```ruby
144
+ # Look up UMA receiver
145
+ receiver = client.quotes.lookup_uma_receiver(
146
+ '$alice@example.com',
147
+ user_id: 'user_id'
148
+ )
149
+
150
+ # Look up external account
151
+ account_info = client.quotes.lookup_external_account('account_id')
152
+
153
+ # Create a quote
154
+ quote = client.quotes.create({
155
+ sourceCurrency: 'USD',
156
+ destinationCurrency: 'BTC',
157
+ amount: '100.00',
158
+ receiverId: 'receiver_id'
159
+ })
160
+
161
+ # List quotes
162
+ quotes = client.quotes.list(limit: 20)
163
+
164
+ # Get quote details
165
+ quote = client.quotes.get('quote_id')
166
+
167
+ # Execute a quote
168
+ result = client.quotes.execute('quote_id')
169
+
170
+ # Retry a failed quote
171
+ client.quotes.retry('quote_id')
172
+ ```
173
+
174
+ ### Transaction Management
175
+
176
+ ```ruby
177
+ # List transactions
178
+ transactions = client.transactions.list(
179
+ limit: 20,
180
+ created_after: '2024-01-01T00:00:00Z'
181
+ )
182
+
183
+ # Get transaction details
184
+ transaction = client.transactions.get('transaction_id')
185
+
186
+ # Approve pending incoming payment
187
+ client.transactions.approve('transaction_id')
188
+
189
+ # Reject pending incoming payment
190
+ client.transactions.reject('transaction_id', reason: 'Duplicate payment')
191
+ ```
192
+
193
+ ### Same-Currency Transfers
194
+
195
+ ```ruby
196
+ # Transfer from external to internal account
197
+ transfer = client.transfers.transfer_in({
198
+ fromAccountId: 'external_account_id',
199
+ toAccountId: 'internal_account_id',
200
+ amount: '100.00',
201
+ currency: 'USD'
202
+ })
203
+
204
+ # Transfer from internal to external account
205
+ transfer = client.transfers.transfer_out({
206
+ fromAccountId: 'internal_account_id',
207
+ toAccountId: 'external_account_id',
208
+ amount: '50.00',
209
+ currency: 'USD'
210
+ })
211
+ ```
212
+
213
+ ### Pagination
214
+
215
+ Many list endpoints support pagination using cursors:
216
+
217
+ ```ruby
218
+ # First page
219
+ response = client.customers.list(limit: 20)
220
+ customers = response['data']
221
+
222
+ # Next page
223
+ if response['hasMore']
224
+ next_response = client.customers.list(
225
+ limit: 20,
226
+ cursor: response['nextCursor']
227
+ )
228
+ end
229
+ ```
230
+
231
+ ### Error Handling
232
+
233
+ The client raises specific exceptions for different error types:
234
+
235
+ ```ruby
236
+ begin
237
+ customer = client.customers.get('invalid_id')
238
+ rescue Lightspark::Grid::NotFoundError => e
239
+ puts "Customer not found: #{e.message}"
240
+ rescue Lightspark::Grid::AuthenticationError => e
241
+ puts "Authentication failed: #{e.message}"
242
+ rescue Lightspark::Grid::BadRequestError => e
243
+ puts "Bad request: #{e.message}"
244
+ rescue Lightspark::Grid::ServerError => e
245
+ puts "Server error: #{e.message}"
246
+ rescue Lightspark::Grid::Error => e
247
+ puts "API error: #{e.message}"
248
+ end
249
+ ```
250
+
251
+ Available exception classes:
252
+ - `Lightspark::Grid::Error` - Base error class
253
+ - `Lightspark::Grid::AuthenticationError` - Invalid API credentials (401)
254
+ - `Lightspark::Grid::BadRequestError` - Invalid request parameters (400)
255
+ - `Lightspark::Grid::NotFoundError` - Resource not found (404)
256
+ - `Lightspark::Grid::ConflictError` - Resource conflict (409)
257
+ - `Lightspark::Grid::ServerError` - Internal server error (500, 501)
258
+
259
+ ## API Coverage
260
+
261
+ This client supports all Grid API endpoints:
262
+
263
+ - **Platform Configuration**: Get/update platform settings
264
+ - **Customers**: Create, list, get, update, delete, KYC links, bulk CSV import
265
+ - **Accounts**: Internal/external account management, Plaid integration
266
+ - **Quotes**: UMA lookup, quote creation, execution, retry
267
+ - **Transactions**: List, get, approve, reject
268
+ - **Transfers**: Same-currency transfers (in/out)
269
+
270
+ ## Base URL
271
+
272
+ The default base URL is `https://api.lightspark.com/grid/2025-10-13`. You can override this during configuration if needed (e.g., for sandbox testing).
273
+
274
+ ## Authentication
275
+
276
+ All requests use HTTP Basic Authentication. The client automatically encodes your API token ID and client secret and includes them in the `Authorization` header.
277
+
278
+ ## Development
279
+
280
+ After checking out the repo, run `bundle install` to install dependencies.
281
+
282
+ ## Contributing
283
+
284
+ Bug reports and pull requests are welcome on GitHub.
285
+
286
+ ## License
287
+
288
+ The gem is available as open source under the terms of the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'ls/grid'
5
+
6
+ # Configure the Lightspark::Grid client
7
+ Lightspark::Grid.configure do |config|
8
+ config.api_token_id = ENV['LIGHTSPARK_API_TOKEN_CLIENT_ID']
9
+ config.api_client_secret = ENV['LIGHTSPARK_API_TOKEN_CLIENT_SECRET']
10
+ end
11
+
12
+ client = Lightspark::Grid.client
13
+
14
+ begin
15
+ # Get platform configuration
16
+ puts "Fetching platform configuration..."
17
+ config = client.config.get
18
+ puts "Platform config: #{config}"
19
+ puts
20
+
21
+ # Create a customer
22
+ puts "Creating a new customer..."
23
+ customer = client.customers.create({
24
+ type: 'INDIVIDUAL',
25
+ firstName: 'John',
26
+ lastName: 'Doe',
27
+ email: 'john.doe@example.com',
28
+ dateOfBirth: '1990-01-01',
29
+ address: {
30
+ street: '123 Main St',
31
+ city: 'San Francisco',
32
+ state: 'CA',
33
+ postalCode: '94102',
34
+ country: 'US'
35
+ }
36
+ })
37
+ puts "Created customer: #{customer['id']}"
38
+ puts
39
+
40
+ # List customers
41
+ puts "Listing customers..."
42
+ customers = client.customers.list(limit: 10)
43
+ puts "Found #{customers['totalCount']} customers"
44
+ puts
45
+
46
+ # Get customer details
47
+ puts "Getting customer details..."
48
+ customer_details = client.customers.get(customer['id'])
49
+ puts "Customer: #{customer_details['firstName']} #{customer_details['lastName']}"
50
+ puts
51
+
52
+ # List customer internal accounts
53
+ puts "Listing customer internal accounts..."
54
+ accounts = client.accounts.list_customer_internal_accounts(
55
+ customer_id: customer['id']
56
+ )
57
+ puts "Found #{accounts['data'].length} internal accounts"
58
+ puts
59
+
60
+ # Create a quote (example)
61
+ puts "Creating a quote..."
62
+ quote = client.quotes.create({
63
+ sourceCurrency: 'USD',
64
+ destinationCurrency: 'BTC',
65
+ sourceAmount: '100.00',
66
+ sendingAccountId: 'sending_account_id',
67
+ receiverUmaAddress: '$alice@example.com'
68
+ })
69
+ puts "Created quote: #{quote['id']}"
70
+ puts
71
+
72
+ # List transactions
73
+ puts "Listing recent transactions..."
74
+ transactions = client.transactions.list(limit: 5)
75
+ puts "Found #{transactions['totalCount']} transactions"
76
+ transactions['data'].each do |tx|
77
+ puts " - #{tx['id']}: #{tx['status']}"
78
+ end
79
+ puts
80
+
81
+ rescue Lightspark::Grid::AuthenticationError => e
82
+ puts "Authentication failed: #{e.message}"
83
+ puts "Please check your API credentials"
84
+ rescue Lightspark::Grid::BadRequestError => e
85
+ puts "Bad request: #{e.message}"
86
+ rescue Lightspark::Grid::NotFoundError => e
87
+ puts "Resource not found: #{e.message}"
88
+ rescue Lightspark::Grid::Error => e
89
+ puts "API error: #{e.message}"
90
+ end
data/lib/ls/.DS_Store ADDED
Binary file
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lightspark
4
+ module Grid
5
+ class Account
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # GET /customers/internal-accounts - List customer internal accounts
11
+ def list_customer_internal_accounts(customer_id:, limit: 20, cursor: nil)
12
+ params = { customerId: customer_id, limit: limit }
13
+ params[:cursor] = cursor if cursor
14
+
15
+ @client.get("/customers/internal-accounts", params: params)
16
+ end
17
+
18
+ # GET /platform/internal-accounts - List platform internal accounts
19
+ def list_platform_internal_accounts(limit: 20, cursor: nil)
20
+ params = { limit: limit }
21
+ params[:cursor] = cursor if cursor
22
+
23
+ @client.get("/platform/internal-accounts", params: params)
24
+ end
25
+
26
+ # GET /customers/external-accounts - List customer external accounts
27
+ def list_customer_external_accounts(customer_id:, limit: 20, cursor: nil)
28
+ params = { customerId: customer_id, limit: limit }
29
+ params[:cursor] = cursor if cursor
30
+
31
+ @client.get("/customers/external-accounts", params: params)
32
+ end
33
+
34
+ # POST /customers/external-accounts - Add customer external account
35
+ def create_customer_external_account(attributes)
36
+ @client.post("/customers/external-accounts", body: attributes)
37
+ end
38
+
39
+ # GET /platform/external-accounts - List platform external accounts
40
+ def list_platform_external_accounts(limit: 20, cursor: nil)
41
+ params = { limit: limit }
42
+ params[:cursor] = cursor if cursor
43
+
44
+ @client.get("/platform/external-accounts", params: params)
45
+ end
46
+
47
+ # POST /platform/external-accounts - Add platform external account
48
+ def create_platform_external_account(attributes)
49
+ @client.post("/platform/external-accounts", body: attributes)
50
+ end
51
+
52
+ # POST /plaid/link-tokens - Create Plaid Link token
53
+ def create_plaid_link_token(customer_id:)
54
+ @client.post("/plaid/link-tokens", body: { customerId: customer_id })
55
+ end
56
+
57
+ # POST /plaid/callback/{plaid_link_token} - Submit Plaid authentication token
58
+ def plaid_callback(plaid_link_token, public_token:)
59
+ @client.post("/plaid/callback/#{plaid_link_token}", body: { publicToken: public_token })
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lightspark
4
+ module Grid
5
+ class Client
6
+ attr_reader :api_token_id, :api_client_secret, :base_url
7
+
8
+ def initialize(api_token_id:, api_client_secret:, base_url: nil)
9
+ @api_token_id = api_token_id
10
+ @api_client_secret = api_client_secret
11
+ @base_url = base_url || Grid.base_url
12
+ end
13
+
14
+ # Resource accessors
15
+ def config
16
+ @config ||= Config.new(self)
17
+ end
18
+
19
+ def customers
20
+ @customers ||= Customer.new(self)
21
+ end
22
+
23
+ def accounts
24
+ @accounts ||= Account.new(self)
25
+ end
26
+
27
+ def quotes
28
+ @quotes ||= Quote.new(self)
29
+ end
30
+
31
+ def transactions
32
+ @transactions ||= Transaction.new(self)
33
+ end
34
+
35
+ def transfers
36
+ @transfers ||= Transfer.new(self)
37
+ end
38
+
39
+ def invitations
40
+ @invitations ||= Invitation.new(self)
41
+ end
42
+
43
+ def webhooks
44
+ @webhooks ||= Webhook.new(self)
45
+ end
46
+
47
+ def sandbox
48
+ @sandbox ||= Sandbox.new(self)
49
+ end
50
+
51
+ def get(path, params: {})
52
+ request(:get, path, params: params)
53
+ end
54
+
55
+ def post(path, body: nil, params: {})
56
+ request(:post, path, body: body, params: params)
57
+ end
58
+
59
+ def patch(path, body: nil, params: {})
60
+ request(:patch, path, body: body, params: params)
61
+ end
62
+
63
+ def delete(path, params: {})
64
+ request(:delete, path, params: params)
65
+ end
66
+
67
+ private
68
+
69
+ def request(method, path, body: nil, params: {})
70
+ uri = build_uri(path, params)
71
+ http = Net::HTTP.new(uri.host, uri.port)
72
+ http.use_ssl = uri.scheme == "https"
73
+
74
+ request = build_request(method, uri, body)
75
+ response = http.request(request)
76
+
77
+ handle_response(response)
78
+ end
79
+
80
+ def build_uri(path, params)
81
+ uri = URI.join(base_url, path)
82
+ unless params.empty?
83
+ uri.query = URI.encode_www_form(params)
84
+ end
85
+ uri
86
+ end
87
+
88
+ def build_request(method, uri, body)
89
+ request_class = case method
90
+ when :get then Net::HTTP::Get
91
+ when :post then Net::HTTP::Post
92
+ when :patch then Net::HTTP::Patch
93
+ when :delete then Net::HTTP::Delete
94
+ else
95
+ raise ArgumentError, "Unsupported HTTP method: #{method}"
96
+ end
97
+
98
+ request = request_class.new(uri)
99
+
100
+ # Set Basic Auth
101
+ credentials = Base64.strict_encode64("#{api_token_id}:#{api_client_secret}")
102
+ request["Authorization"] = "Basic #{credentials}"
103
+
104
+ # Set headers
105
+ request["Accept"] = "application/json"
106
+
107
+ if body
108
+ request["Content-Type"] = "application/json"
109
+ request.body = JSON.generate(body)
110
+ end
111
+
112
+ request
113
+ end
114
+
115
+ def handle_response(response)
116
+ case response
117
+ when Net::HTTPSuccess, Net::HTTPCreated, Net::HTTPAccepted
118
+ response.body.empty? ? {} : JSON.parse(response.body)
119
+ when Net::HTTPBadRequest
120
+ raise BadRequestError, parse_error_message(response)
121
+ when Net::HTTPUnauthorized
122
+ raise AuthenticationError, "Invalid API credentials"
123
+ when Net::HTTPNotFound
124
+ raise NotFoundError, "Resource not found"
125
+ when Net::HTTPConflict
126
+ raise ConflictError, parse_error_message(response)
127
+ when Net::HTTPServerError, Net::HTTPNotImplemented
128
+ raise ServerError, parse_error_message(response)
129
+ else
130
+ raise Error, "HTTP #{response.code}: #{response.message}"
131
+ end
132
+ end
133
+
134
+ def parse_error_message(response)
135
+ return response.message if response.body.nil? || response.body.empty?
136
+
137
+ begin
138
+ error_data = JSON.parse(response.body)
139
+ error_data["message"] || error_data["error"] || response.message
140
+ rescue JSON::ParserError
141
+ response.message
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lightspark
4
+ module Grid
5
+ class Config
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # GET /config - Retrieve platform settings
11
+ def get
12
+ @client.get("/config")
13
+ end
14
+
15
+ # PATCH /config - Update platform configuration
16
+ def update(attributes)
17
+ @client.patch("/config", body: attributes)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lightspark
4
+ module Grid
5
+ class Customer
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # POST /customers - Create individual or business customer
11
+ def create(attributes)
12
+ @client.post("/customers", body: attributes)
13
+ end
14
+
15
+ # GET /customers - List customers with filters
16
+ def list(limit: 20, cursor: nil, created_after: nil, created_before: nil)
17
+ params = { limit: limit }
18
+ params[:cursor] = cursor if cursor
19
+ params[:createdAfter] = created_after if created_after
20
+ params[:createdBefore] = created_before if created_before
21
+
22
+ @client.get("/customers", params: params)
23
+ end
24
+
25
+ # GET /customers/{customerId} - Get customer details
26
+ def get(customer_id)
27
+ @client.get("/customers/#{customer_id}")
28
+ end
29
+
30
+ # PATCH /customers/{customerId} - Update customer info
31
+ def update(customer_id, attributes)
32
+ @client.patch("/customers/#{customer_id}", body: attributes)
33
+ end
34
+
35
+ # DELETE /customers/{customerId} - Delete customer
36
+ def delete(customer_id)
37
+ @client.delete("/customers/#{customer_id}")
38
+ end
39
+
40
+ # GET /customers/kyc-link - Generate KYC onboarding URL
41
+ def kyc_link(customer_id:, redirect_url: nil)
42
+ params = { customerId: customer_id }
43
+ params[:redirectUrl] = redirect_url if redirect_url
44
+
45
+ @client.get("/customers/kyc-link", params: params)
46
+ end
47
+
48
+ # POST /customers/bulk/csv - Upload customers via CSV file
49
+ def bulk_create_csv(csv_content)
50
+ @client.post("/customers/bulk/csv", body: { csv: csv_content })
51
+ end
52
+
53
+ # GET /customers/bulk/jobs/{jobId} - Check bulk import job status
54
+ def bulk_job_status(job_id)
55
+ @client.get("/customers/bulk/jobs/#{job_id}")
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lightspark
4
+ module Grid
5
+ class Invitation
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # POST /invitations - Create UMA invitation
11
+ def create(attributes)
12
+ @client.post("/invitations", body: attributes)
13
+ end
14
+
15
+ # GET /invitations/{invitationCode} - Get invitation details
16
+ def get(invitation_code)
17
+ @client.get("/invitations/#{invitation_code}")
18
+ end
19
+
20
+ # POST /invitations/{invitationCode}/claim - Claim invitation
21
+ def claim(invitation_code, attributes = {})
22
+ @client.post("/invitations/#{invitation_code}/claim", body: attributes)
23
+ end
24
+
25
+ # POST /invitations/{invitationCode}/cancel - Cancel invitation
26
+ def cancel(invitation_code)
27
+ @client.post("/invitations/#{invitation_code}/cancel")
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lightspark
4
+ module Grid
5
+ class Quote
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # GET /receiver/uma/{receiverUmaAddress} - Look up UMA address for payment
11
+ def lookup_uma_receiver(receiver_uma_address, user_id:)
12
+ params = { userId: user_id }
13
+ @client.get("/receiver/uma/#{receiver_uma_address}", params: params)
14
+ end
15
+
16
+ # GET /receiver/external-account/{accountId} - Look up external account details
17
+ def lookup_external_account(account_id)
18
+ @client.get("/receiver/external-account/#{account_id}")
19
+ end
20
+
21
+ # POST /quotes - Create transfer quote
22
+ def create(attributes)
23
+ @client.post("/quotes", body: attributes)
24
+ end
25
+
26
+ # GET /quotes - List quotes with filters
27
+ def list(limit: 20, cursor: nil, created_after: nil, created_before: nil)
28
+ params = { limit: limit }
29
+ params[:cursor] = cursor if cursor
30
+ params[:createdAfter] = created_after if created_after
31
+ params[:createdBefore] = created_before if created_before
32
+
33
+ @client.get("/quotes", params: params)
34
+ end
35
+
36
+ # GET /quotes/{quoteId} - Retrieve quote details
37
+ def get(quote_id)
38
+ @client.get("/quotes/#{quote_id}")
39
+ end
40
+
41
+ # POST /quotes/{quoteId}/execute - Execute confirmed quote
42
+ def execute(quote_id, attributes = {})
43
+ @client.post("/quotes/#{quote_id}/execute", body: attributes)
44
+ end
45
+
46
+ # POST /quotes/{quoteId}/retry - Retry incomplete payment
47
+ def retry(quote_id)
48
+ @client.post("/quotes/#{quote_id}/retry")
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lightspark
4
+ module Grid
5
+ class Sandbox
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # POST /sandbox/send - Simulate sending funds (sandbox only)
11
+ def send(attributes)
12
+ @client.post("/sandbox/send", body: attributes)
13
+ end
14
+
15
+ # POST /sandbox/receive - Simulate receiving funds (sandbox only)
16
+ def receive(attributes)
17
+ @client.post("/sandbox/receive", body: attributes)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lightspark
4
+ module Grid
5
+ class Transaction
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # GET /transactions - List transactions with filters
11
+ def list(limit: 20, cursor: nil, created_after: nil, created_before: nil)
12
+ params = { limit: limit }
13
+ params[:cursor] = cursor if cursor
14
+ params[:createdAfter] = created_after if created_after
15
+ params[:createdBefore] = created_before if created_before
16
+
17
+ @client.get("/transactions", params: params)
18
+ end
19
+
20
+ # GET /transactions/{transactionId} - Get transaction details
21
+ def get(transaction_id)
22
+ @client.get("/transactions/#{transaction_id}")
23
+ end
24
+
25
+ # POST /transactions/{transactionId}/approve - Approve pending incoming payment
26
+ def approve(transaction_id, attributes = {})
27
+ @client.post("/transactions/#{transaction_id}/approve", body: attributes)
28
+ end
29
+
30
+ # POST /transactions/{transactionId}/reject - Reject pending incoming payment
31
+ def reject(transaction_id, reason: nil)
32
+ body = reason ? { reason: reason } : {}
33
+ @client.post("/transactions/#{transaction_id}/reject", body: body)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lightspark
4
+ module Grid
5
+ class Transfer
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # POST /transfer-in - Transfer from external to internal account
11
+ def transfer_in(attributes)
12
+ @client.post("/transfer-in", body: attributes)
13
+ end
14
+
15
+ # POST /transfer-out - Transfer from internal to external account
16
+ def transfer_out(attributes)
17
+ @client.post("/transfer-out", body: attributes)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lightspark
4
+ module Grid
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lightspark
4
+ module Grid
5
+ class Webhook
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # POST /webhooks/test - Send test webhook
11
+ def test(event_type:)
12
+ @client.post("/webhooks/test", body: { eventType: event_type })
13
+ end
14
+ end
15
+ end
16
+ end
data/lib/ls/grid.rb ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+ require "base64"
7
+
8
+ require_relative "grid/version"
9
+ require_relative "grid/client"
10
+ require_relative "grid/config"
11
+ require_relative "grid/customer"
12
+ require_relative "grid/account"
13
+ require_relative "grid/quote"
14
+ require_relative "grid/transaction"
15
+ require_relative "grid/transfer"
16
+ require_relative "grid/invitation"
17
+ require_relative "grid/webhook"
18
+ require_relative "grid/sandbox"
19
+
20
+ module Lightspark
21
+ module Grid
22
+ class Error < StandardError; end
23
+ class AuthenticationError < Error; end
24
+ class NotFoundError < Error; end
25
+ class BadRequestError < Error; end
26
+ class ConflictError < Error; end
27
+ class ServerError < Error; end
28
+
29
+ class << self
30
+ attr_accessor :api_token_id, :api_client_secret, :base_url
31
+
32
+ def configure
33
+ yield self
34
+ end
35
+
36
+ def client
37
+ @client ||= Client.new(
38
+ api_token_id: api_token_id,
39
+ api_client_secret: api_client_secret,
40
+ base_url: base_url
41
+ )
42
+ end
43
+ end
44
+
45
+ # Default base URL
46
+ self.base_url = "https://api.lightspark.com/grid/2025-10-13"
47
+ end
48
+ end
data/sig/ls/grid.rbs ADDED
@@ -0,0 +1,6 @@
1
+ module Lightspark
2
+ module Grid
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ls-grid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kang-Kyu Lee
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.3.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.3.0
26
+ description: Use Lightspark Grid API for money moving with ls-grid gem
27
+ email:
28
+ - kangkyu1111@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - CHANGELOG.md
34
+ - CODE_OF_CONDUCT.md
35
+ - LICENSE.txt
36
+ - README.md
37
+ - Rakefile
38
+ - examples/grid_example.rb
39
+ - lib/ls/.DS_Store
40
+ - lib/ls/grid.rb
41
+ - lib/ls/grid/account.rb
42
+ - lib/ls/grid/client.rb
43
+ - lib/ls/grid/config.rb
44
+ - lib/ls/grid/customer.rb
45
+ - lib/ls/grid/invitation.rb
46
+ - lib/ls/grid/quote.rb
47
+ - lib/ls/grid/sandbox.rb
48
+ - lib/ls/grid/transaction.rb
49
+ - lib/ls/grid/transfer.rb
50
+ - lib/ls/grid/version.rb
51
+ - lib/ls/grid/webhook.rb
52
+ - sig/ls/grid.rbs
53
+ homepage: https://github.com/kangkyu/lightspark-grid
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ homepage_uri: https://github.com/kangkyu/lightspark-grid
58
+ changelog_uri: https://github.com/kangkyu/lightspark-grid/blob/master/CHANGELOG.md
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 3.2.0
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.7.2
74
+ specification_version: 4
75
+ summary: Lightspark Grid API Ruby client
76
+ test_files: []