diff --git a/AGENTS.md b/AGENTS.md index c46a340..aeb6636 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,9 +35,9 @@ It provides a clean, object-oriented interface for payment processing, subscript - Handles authentication via HMAC-SHA512 signatures - Provides methods for all Solidgate API endpoints: - Payment operations: `create_payment`, `get_payment`, `capture_payment`, `void_payment`, `refund_payment`, `settle_payment` - - Subscription operations: `create_subscription`, `subscription_status`, `switch_subscription_product`, `update_subscription_pause`, `create_subscription_pause`, `delete_subscription_pause`, `cancel_subscription`, `restore_subscription` + - Subscription operations: `create_subscription`, `subscription_status`, `switch_subscription_product`, `update_subscription_pause`, `create_subscription_pause`, `delete_subscription_pause`, `cancel_subscription`, `restore_subscription`, `update_subscription_payment_method` - Product operations: `create_product`, `update_product`, `create_price`, `products`, `product_prices`, `update_product_price` - - Utility methods: `generate_intent`, `generate_signature`, `refund`, `order_status` + - Utility methods: `generate_intent`, `generate_signature`, `refund`, `alt_refund`, `order_status`, `apm_order_status`, `make_card_recurring`, `make_apm_recurring` - Private methods for HTTP operations: `get`, `post`, `patch`, `delete`, `request` - Encryption: `encrypt_payload` for payment intent generation (AES-256-CBC) @@ -65,7 +65,7 @@ It provides a clean, object-oriented interface for payment processing, subscript - `Solidgate::ValidationError` - Parameter validation (includes errors hash) #### `lib/solidgate/version.rb` -- Version constant: `Solidgate::VERSION = "0.1.17"` +- Version constant: `Solidgate::VERSION = "0.2.0"` ### Test Structure diff --git a/CHANGELOG.md b/CHANGELOG.md index ca66f82..5c2e945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.0] - 2026-05-05 +### Added +- `apm_order_status` endpoint for checking alternative payment method order status. +- Spec coverage for previously untested client methods added in this worktree. + +### Fixed +- Recurring payment spec coverage bug affecting the recurring endpoint test suite. + +### Changed +- Documentation updates and version bump for the `0.2.0` release. + ## [0.1.17] - 2026-03-03 ### Added - `update_product` endpoint (`Solidgate::Client#update_product`) to modify product attributes. @@ -18,6 +29,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `alt_refund` endpoint to create refunds using an alternative payment method (`Solidgate::Client#alt_refund`). +## [0.1.15] - 2026-02-20 +### Added +- `make_apm_recurring` endpoint to create recurring alternative payment method charges (`Solidgate::Client#make_apm_recurring`). + +## [0.1.14] - 2026-02-17 +### Added +- `make_card_recurring` endpoint to create recurring card charges (`Solidgate::Client#make_card_recurring`). + ## [0.1.13] - 2026-02-13 ### Added - `order_status` endpoint to check order/payment status on `pay.solidgate.com` (`Solidgate::Client#order_status`). @@ -75,9 +94,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Thread-safe configuration - Comprehensive documentation and examples -[Unreleased]: https://github.com/carrfane/solidgate-ruby-sdk/compare/v0.1.17...HEAD +[Unreleased]: https://github.com/carrfane/solidgate-ruby-sdk/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/carrfane/solidgate-ruby-sdk/compare/v0.1.17...v0.2.0 [0.1.17]: https://github.com/carrfane/solidgate-ruby-sdk/releases/tag/v0.1.17 [0.1.16]: https://github.com/carrfane/solidgate-ruby-sdk/releases/tag/v0.1.16 +[0.1.15]: https://github.com/carrfane/solidgate-ruby-sdk/compare/v0.1.14...v0.1.15 +[0.1.14]: https://github.com/carrfane/solidgate-ruby-sdk/compare/v0.1.13...v0.1.14 [0.1.13]: https://github.com/carrfane/solidgate-ruby-sdk/releases/tag/v0.1.13 [0.1.12]: https://github.com/carrfane/solidgate-ruby-sdk/releases/tag/v0.1.12 [0.1.11]: https://github.com/carrfane/solidgate-ruby-sdk/releases/tag/v0.1.11 diff --git a/Gemfile.lock b/Gemfile.lock index 6b72de9..a917ad6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - solidgate-ruby-sdk (0.1.17) + solidgate-ruby-sdk (0.2.0) faraday faraday-multipart diff --git a/README.md b/README.md index 0b4e14c..a687a27 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,32 @@ client.refund_payment('payment_id_123', amount: 500, reason: 'Customer requested # Refund by order ID (pay.solidgate.com) client.refund(order_id: 'order_123', amount: 1000) +# Refund an alternative payment method order (gate.solidgate.com) +client.alt_refund(order_id: 'apm_order_123', amount: 750) + # Check order status (pay.solidgate.com) client.order_status(order_id: 'order_123') + +# Check alternative payment method order status (gate.solidgate.com) +client.apm_order_status(order_id: 'apm_order_123') + +# Charge a saved card payment method for a recurring payment +client.make_card_recurring( + order_id: 'renewal_order_123', + amount: 1999, + currency: 'USD', + customer_email: 'customer@example.com', + token: 'card_tok_abc123' +) + +# Charge a saved APM payment method for a recurring payment +client.make_apm_recurring( + order_id: 'apm_renewal_123', + amount: 1499, + currency: 'EUR', + customer_email: 'customer@example.com', + token: 'apm_tok_xyz789' +) ``` ### Subscription Management diff --git a/docs/payment-integration-guide.md b/docs/payment-integration-guide.md index 881c7d1..c854b9e 100644 --- a/docs/payment-integration-guide.md +++ b/docs/payment-integration-guide.md @@ -1,6 +1,6 @@ # Solidgate Ruby SDK Integration Guide (Payments) -> Target version: `solidgate-ruby-sdk` `0.1.17` +> Target version: `solidgate-ruby-sdk` `0.2.0` > Audience: engineers and LLM agents integrating Solidgate payments in Ruby apps. ## 1) What this SDK provides @@ -107,11 +107,12 @@ refunded = client.refund_payment(payment_id, amount: 300, reason: "partial_refun - `void_payment(payment_id)` -> `POST /v1/charge/:payment_id/void` - `refund_payment(payment_id, params = {})` -> `POST /v1/charge/:payment_id/refund` -### Refund/status via pay domain +### Refund and status helper routes - `refund(params)` -> `POST https://pay.solidgate.com/api/v1/refund` - typically `order_id`, optional `amount` - `order_status(params)` -> `POST https://pay.solidgate.com/api/v1/status` +- `apm_order_status(params)` -> `POST https://gate.solidgate.com/api/v1/status` ### Alternative payment routes @@ -242,7 +243,7 @@ If an LLM agent is integrating this SDK into another project, follow this sequen --- -## 10) Known quirks in current SDK (v0.1.17) +## 10) Known quirks in current SDK (v0.2.0) - `settle_payment` currently does **not** call an API endpoint; it returns `config.api_url`. - `README.md` examples may not fully match actual method signatures in code. diff --git a/lib/solidgate/client.rb b/lib/solidgate/client.rb index 979f7dd..a71c71c 100644 --- a/lib/solidgate/client.rb +++ b/lib/solidgate/client.rb @@ -417,6 +417,20 @@ def order_status(params) post('/api/v1/status', body: params, base_url: "https://pay.solidgate.com") end + # Retrieves order status from the Solidgate gate domain for APM transactions. + # + # @param params [Hash] status request parameters: + # - :order_id [String] unique order identifier + # - :payment_id [String] optional payment identifier + # @return [Hash] order status response + # @raise [InvalidRequestError] if params are invalid + # + # @example Get APM order status + # client.apm_order_status(order_id: 'order_123') + def apm_order_status(params) + post('/api/v1/status', body: params, base_url: "https://gate.solidgate.com") + end + def make_card_recurring(params) post('/api/v1/recurring', body: params, base_url: "https://pay.solidgate.com") end diff --git a/lib/solidgate/version.rb b/lib/solidgate/version.rb index 4ecc662..ea28717 100644 --- a/lib/solidgate/version.rb +++ b/lib/solidgate/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Solidgate - VERSION = "0.1.17" + VERSION = "0.2.0" end diff --git a/spec/solidgate/client_spec.rb b/spec/solidgate/client_spec.rb index 0466fc1..55aaa8a 100644 --- a/spec/solidgate/client_spec.rb +++ b/spec/solidgate/client_spec.rb @@ -443,25 +443,209 @@ end end + describe "#alt_refund" do + let(:refund_params) do + { + order_id: "order_123", + amount: 1000 + } + end + + before do + stub_request(:post, /gate\.solidgate\.com/).to_return( + status: 200, + body: success_response.to_json, + headers: { "Content-Type" => "application/json" } + ) + end + + it "sends POST request to https://gate.solidgate.com/api/v1/refund" do + client.alt_refund(refund_params) + + expect(WebMock).to have_requested(:post, "https://gate.solidgate.com/api/v1/refund") + .with(body: refund_params.to_json) + end + + it "uses the gate subdomain instead of subscriptions or pay subdomains" do + client.alt_refund(refund_params) + + expect(WebMock).not_to have_requested(:post, /subscriptions\.solidgate\.com/) + expect(WebMock).not_to have_requested(:post, /pay\.solidgate\.com/) + expect(WebMock).to have_requested(:post, /gate\.solidgate\.com/) + end + + it "includes Merchant header with public key" do + client.alt_refund(refund_params) + + expect(WebMock).to have_requested(:post, "https://gate.solidgate.com/api/v1/refund") + .with(headers: { "Merchant" => public_key }) + end + + it "includes Signature header" do + client.alt_refund(refund_params) + + expect(WebMock).to have_requested(:post, "https://gate.solidgate.com/api/v1/refund") + .with { |req| !req.headers["Signature"].nil? && !req.headers["Signature"].empty? } + end + + it "returns refund response" do + result = client.alt_refund(refund_params) + expect(result).to eq(success_response) + end + end + + describe "#order_status" do + let(:status_params) do + { + order_id: "order_123" + } + end + + before do + stub_request(:post, /pay\.solidgate\.com/).to_return( + status: 200, + body: success_response.to_json, + headers: { "Content-Type" => "application/json" } + ) + end + + it "sends POST request to https://pay.solidgate.com/api/v1/status" do + client.order_status(status_params) + + expect(WebMock).to have_requested(:post, "https://pay.solidgate.com/api/v1/status") + .with(body: status_params.to_json) + end + + it "uses the pay subdomain instead of subscriptions or gate subdomains" do + client.order_status(status_params) + + expect(WebMock).not_to have_requested(:post, /subscriptions\.solidgate\.com/) + expect(WebMock).not_to have_requested(:post, /gate\.solidgate\.com/) + expect(WebMock).to have_requested(:post, /pay\.solidgate\.com/) + end + + it "includes Merchant header with public key" do + client.order_status(status_params) + + expect(WebMock).to have_requested(:post, "https://pay.solidgate.com/api/v1/status") + .with(headers: { "Merchant" => public_key }) + end + + it "includes Signature header" do + client.order_status(status_params) + + expect(WebMock).to have_requested(:post, "https://pay.solidgate.com/api/v1/status") + .with { |req| !req.headers["Signature"].nil? && !req.headers["Signature"].empty? } + end + + it "returns order status response" do + result = client.order_status(status_params) + expect(result).to eq(success_response) + end + end + + describe "#apm_order_status" do + let(:status_params) do + { + order_id: "order_123" + } + end + + before do + stub_request(:post, /gate\.solidgate\.com/).to_return( + status: 200, + body: success_response.to_json, + headers: { "Content-Type" => "application/json" } + ) + end + + it "sends POST request to https://gate.solidgate.com/api/v1/status" do + client.apm_order_status(status_params) + + expect(WebMock).to have_requested(:post, "https://gate.solidgate.com/api/v1/status") + .with(body: status_params.to_json) + end + + it "uses the gate subdomain instead of subscriptions or pay subdomains" do + client.apm_order_status(status_params) + + expect(WebMock).not_to have_requested(:post, /subscriptions\.solidgate\.com/) + expect(WebMock).not_to have_requested(:post, /pay\.solidgate\.com/) + expect(WebMock).to have_requested(:post, /gate\.solidgate\.com/) + end + + it "includes Merchant header with public key" do + client.apm_order_status(status_params) + + expect(WebMock).to have_requested(:post, "https://gate.solidgate.com/api/v1/status") + .with(headers: { "Merchant" => public_key }) + end + + it "includes Signature header" do + client.apm_order_status(status_params) + + expect(WebMock).to have_requested(:post, "https://gate.solidgate.com/api/v1/status") + .with { |req| !req.headers["Signature"].nil? && !req.headers["Signature"].empty? } + end + + it "returns order status response" do + result = client.apm_order_status(status_params) + expect(result).to eq(success_response) + end + end + describe '#make_card_recurring' do let(:recurring_params) do { - payment_method: "paypal-vault", - token: "baf2ff5c5a125aeabb4b80d7b983f66f3abf5dbb8d939df48b40755674eddceee78084eab5fa9c15a339c94f1ad2b30cf299", - order_id: "923bb4e6-4a5f-41ec-81fb-28eb8a152e55", + order_id: "order_123", + amount: 1020, + currency: "EUR", + token: "token_123" + } + end + + before do + stub_request(:post, /pay\.solidgate\.com/).to_return( + status: 200, + body: success_response.to_json, + headers: { "Content-Type" => "application/json" } + ) + end + + it "sends POST request to https://pay.solidgate.com/api/v1/recurring" do + client.make_card_recurring(recurring_params) + + expect(WebMock).to have_requested(:post, "https://pay.solidgate.com/api/v1/recurring") + .with(body: recurring_params.to_json) + end + + it "includes Merchant header with public key" do + client.make_card_recurring(recurring_params) + + expect(WebMock).to have_requested(:post, "https://pay.solidgate.com/api/v1/recurring") + .with(headers: { "Merchant" => public_key }) + end + + it "includes Signature header" do + client.make_card_recurring(recurring_params) + + expect(WebMock).to have_requested(:post, "https://pay.solidgate.com/api/v1/recurring") + .with { |req| !req.headers["Signature"].nil? && !req.headers["Signature"].empty? } + end + + it "returns recurring response" do + result = client.make_card_recurring(recurring_params) + expect(result).to eq(success_response) + end + end + + describe '#make_apm_recurring' do + let(:recurring_params) do + { + order_id: "order_123", amount: 1020, currency: "EUR", - order_description: "Premium package", - order_date: "2025-12-21 11:21:30", - customer_account_id: "93a1c659-288d-4d62-929d-10e241078faa", - customer_email: "example@example.com", - customer_date_of_birth: "1988-11-21", - ip_address: "203.0.113.0", - platform: "WEB", - order_metadata: { - coupon_code: "NY2025", - partner_id: "123989" - } + token: "token_123" } end @@ -473,12 +657,31 @@ ) end - it "sends POST request to https://pay.solidgate.com/api/v1/refund" do + it "sends POST request to https://gate.solidgate.com/api/v1/recurring" do client.make_apm_recurring(recurring_params) expect(WebMock).to have_requested(:post, "https://gate.solidgate.com/api/v1/recurring") .with(body: recurring_params.to_json) end + + it "includes Merchant header with public key" do + client.make_apm_recurring(recurring_params) + + expect(WebMock).to have_requested(:post, "https://gate.solidgate.com/api/v1/recurring") + .with(headers: { "Merchant" => public_key }) + end + + it "includes Signature header" do + client.make_apm_recurring(recurring_params) + + expect(WebMock).to have_requested(:post, "https://gate.solidgate.com/api/v1/recurring") + .with { |req| !req.headers["Signature"].nil? && !req.headers["Signature"].empty? } + end + + it "returns recurring response" do + result = client.make_apm_recurring(recurring_params) + expect(result).to eq(success_response) + end end # ==================== Base URL Override ==================== @@ -548,6 +751,42 @@ end end + describe "#update_product" do + let(:product_id) { "prod_123" } + let(:product_params) do + { + name: "Updated Premium Plan", + description: "Updated access to premium features" + } + end + + it "sends PATCH request to /api/v1/products/:id" do + client.update_product(product_id, product_params) + + expect(WebMock).to have_requested(:patch, "https://subscriptions.solidgate.com/api/v1/products/#{product_id}") + .with(body: product_params.to_json) + end + + it "includes Merchant header with public key" do + client.update_product(product_id, product_params) + + expect(WebMock).to have_requested(:patch, "https://subscriptions.solidgate.com/api/v1/products/#{product_id}") + .with(headers: { "Merchant" => public_key }) + end + + it "includes Signature header" do + client.update_product(product_id, product_params) + + expect(WebMock).to have_requested(:patch, "https://subscriptions.solidgate.com/api/v1/products/#{product_id}") + .with { |req| !req.headers["Signature"].nil? && !req.headers["Signature"].empty? } + end + + it "returns updated product" do + result = client.update_product(product_id, product_params) + expect(result).to eq(success_response) + end + end + describe "#create_price" do let(:product_id) { "prod_123" } let(:price_params) do @@ -599,6 +838,49 @@ end end + describe "#update_product_price" do + let(:product_id) { "product_id_123" } + let(:price_id) { "price_id_456" } + let(:price_params) do + { + status: "active", + product_price: 1000, + trial_price: 500, + currency: "USD", + country: "USA" + } + end + + it "sends PATCH request to /api/v1/products/:product_id/prices/:price_id" do + client.update_product_price(product_id, price_id, price_params) + + expect(WebMock).to have_requested(:patch, + "https://subscriptions.solidgate.com/api/v1/products/#{product_id}/prices/#{price_id}") + .with(body: price_params.to_json) + end + + it "includes Merchant header with public key" do + client.update_product_price(product_id, price_id, price_params) + + expect(WebMock).to have_requested(:patch, + "https://subscriptions.solidgate.com/api/v1/products/#{product_id}/prices/#{price_id}") + .with(headers: { "Merchant" => public_key }) + end + + it "includes Signature header" do + client.update_product_price(product_id, price_id, price_params) + + expect(WebMock).to have_requested(:patch, + "https://subscriptions.solidgate.com/api/v1/products/#{product_id}/prices/#{price_id}") + .with { |req| !req.headers["Signature"].nil? && !req.headers["Signature"].empty? } + end + + it "returns updated price" do + result = client.update_product_price(product_id, price_id, price_params) + expect(result).to eq(success_response) + end + end + # ==================== Intent & Signature Methods ==================== describe "#generate_intent" do