# frozen_string_literal: true require "faraday" require "json" require "uri" module SynorEconomics # Synor Economics SDK client for Ruby. # Pricing, billing, staking, and discount management. class Client attr_reader :closed def initialize(config) @config = config @closed = false @conn = Faraday.new(url: config.endpoint) do |f| f.request :json f.response :json f.options.timeout = config.timeout f.headers["Authorization"] = "Bearer #{config.api_key}" f.headers["Content-Type"] = "application/json" f.headers["X-SDK-Version"] = "ruby/#{VERSION}" end end # ==================== Pricing Operations ==================== def get_pricing(service: nil) path = service ? "/pricing?service=#{service}" : "/pricing" response = get(path) (response["pricing"] || []).map { |p| parse_service_pricing(p) } end def get_price(service:, usage:) body = { service: service, usage: usage_to_hash(usage) } response = post("/pricing/calculate", body) parse_price_result(response) end def estimate_cost(plan:) body = { plan: plan.map { |p| usage_plan_item_to_hash(p) } } response = post("/pricing/estimate", body) parse_cost_estimate(response) end # ==================== Billing Operations ==================== def get_usage(period: nil) path = period ? "/billing/usage?period=#{period}" : "/billing/usage" response = get(path) parse_usage(response) end def get_invoices response = get("/billing/invoices") (response["invoices"] || []).map { |i| parse_invoice(i) } end def get_invoice(invoice_id) response = get("/billing/invoices/#{encode(invoice_id)}") parse_invoice(response) end def get_balance response = get("/billing/balance") parse_account_balance(response) end # ==================== Staking Operations ==================== def stake(amount:, duration_days: nil) body = { amount: amount } body[:duration_days] = duration_days if duration_days response = post("/staking/stake", body) parse_stake_receipt(response) end def unstake(stake_id) response = post("/staking/#{encode(stake_id)}/unstake", {}) parse_unstake_receipt(response) end def get_stakes response = get("/staking") (response["stakes"] || []).map { |s| parse_stake(s) } end def get_stake(stake_id) response = get("/staking/#{encode(stake_id)}") parse_stake(response) end def get_staking_rewards response = get("/staking/rewards") parse_staking_rewards(response) end def claim_rewards response = post("/staking/rewards/claim", {}) parse_stake_receipt(response) end # ==================== Discount Operations ==================== def apply_discount(code) response = post("/discounts/apply", { code: code }) parse_discount_result(response) end def remove_discount(code) delete("/discounts/#{encode(code)}") end def get_discounts response = get("/discounts") (response["discounts"] || []).map { |d| parse_discount(d) } end # ==================== Lifecycle ==================== def health_check response = get("/health") response["status"] == "healthy" rescue StandardError false end def close @closed = true @conn.close if @conn.respond_to?(:close) end private def get(path, params = {}) execute { @conn.get(path, params).body } end def post(path, body) execute { @conn.post(path, body).body } end def delete(path) execute { @conn.delete(path).body } end def execute raise ClientClosedError, "Client has been closed" if @closed last_error = nil @config.retries.times do |attempt| begin response = yield check_error(response) if response.is_a?(Hash) return response rescue StandardError => e last_error = e sleep(2**attempt) if attempt < @config.retries - 1 end end raise last_error end def check_error(response) return unless response["error"] || (response["code"] && response["message"]) message = response["message"] || response["error"] || "Unknown error" code = response["code"] status = response["status_code"] || 0 raise HttpError.new(message, status_code: status, code: code) end def encode(str) URI.encode_www_form_component(str) end # Parsing methods def parse_service_pricing(data) ServicePricing.new( service: data["service"], unit: data["unit"], price_per_unit: data["price_per_unit"], currency: data["currency"], minimum: data["minimum"], maximum: data["maximum"] ) end def parse_price_result(data) PriceResult.new( service: data["service"], amount: data["amount"], currency: data["currency"], usage: data["usage"] ? parse_usage_metrics(data["usage"]) : nil ) end def parse_usage_metrics(data) UsageMetrics.new( service: data["service"], compute_units: data["compute_units"], storage_bytes: data["storage_bytes"], bandwidth_bytes: data["bandwidth_bytes"], duration_seconds: data["duration_seconds"], requests: data["requests"] ) end def parse_cost_estimate(data) CostEstimate.new( total: data["total"], currency: data["currency"], breakdown: (data["breakdown"] || []).map { |b| CostItem.new(service: b["service"], amount: b["amount"]) }, discount_applied: data["discount_applied"], period: data["period"] ) end def parse_usage(data) Usage.new( period: data["period"], start_date: data["start_date"], end_date: data["end_date"], items: (data["items"] || []).map { |i| parse_usage_item(i) }, total_cost: data["total_cost"], currency: data["currency"] ) end def parse_usage_item(data) UsageItem.new( service: data["service"], compute_units: data["compute_units"], storage_bytes: data["storage_bytes"], bandwidth_bytes: data["bandwidth_bytes"], requests: data["requests"], cost: data["cost"] ) end def parse_invoice(data) Invoice.new( id: data["id"], date: data["date"], due_date: data["due_date"], status: data["status"], lines: (data["lines"] || []).map { |l| parse_invoice_line(l) }, subtotal: data["subtotal"], discount: data["discount"], tax: data["tax"], total: data["total"], currency: data["currency"], pdf_url: data["pdf_url"] ) end def parse_invoice_line(data) InvoiceLine.new( service: data["service"], description: data["description"], quantity: data["quantity"], unit_price: data["unit_price"], amount: data["amount"] ) end def parse_account_balance(data) AccountBalance.new( available: data["available"], pending: data["pending"], staked: data["staked"], total: data["total"], currency: data["currency"] ) end def parse_stake(data) Stake.new( id: data["id"], amount: data["amount"], staked_at: data["staked_at"], unlock_at: data["unlock_at"], status: data["status"], rewards_earned: data["rewards_earned"], apy: data["apy"] ) end def parse_stake_receipt(data) StakeReceipt.new( id: data["id"], amount: data["amount"], tx_hash: data["tx_hash"], staked_at: data["staked_at"], unlock_at: data["unlock_at"], apy: data["apy"] ) end def parse_unstake_receipt(data) UnstakeReceipt.new( id: data["id"], amount: data["amount"], tx_hash: data["tx_hash"], unstaked_at: data["unstaked_at"], available_at: data["available_at"] ) end def parse_staking_rewards(data) StakingRewards.new( pending: data["pending"], claimed: data["claimed"], total: data["total"], current_apy: data["current_apy"], last_claim: data["last_claim"], next_distribution: data["next_distribution"] ) end def parse_discount(data) Discount.new( code: data["code"], type: data["type"], value: data["value"], description: data["description"], applicable_services: data["applicable_services"], valid_from: data["valid_from"], valid_until: data["valid_until"], max_uses: data["max_uses"], current_uses: data["current_uses"], active: data["active"] ) end def parse_discount_result(data) DiscountResult.new( discount: parse_discount(data["discount"]), savings_estimate: data["savings_estimate"], applied_at: data["applied_at"] ) end # Conversion methods def usage_to_hash(usage) { service: usage.service, compute_units: usage.compute_units, storage_bytes: usage.storage_bytes, bandwidth_bytes: usage.bandwidth_bytes, duration_seconds: usage.duration_seconds, requests: usage.requests } end def usage_plan_item_to_hash(item) { service: item.service, projected_usage: usage_to_hash(item.projected_usage), period: item.period } end end end