# frozen_string_literal: true require "braintree" require "bigdecimal/util" require "countries" require "date" require "delegate" require "dhall" require "forwardable" require "pg" require "redis" require "roda" require "uri" require "json" if ENV["RACK_ENV"] == "development" require "pry-rescue" use PryRescue::Rack end require_relative "lib/auto_top_up_repo" require_relative "lib/customer" require_relative "lib/three_d_secure_repo" require_relative "lib/electrum" require_relative "lib/transaction" require_relative "lib/credit_card_customer_gateway" require "sentry-ruby" Sentry.init do |config| config.traces_sample_rate = 0.01 end use Sentry::Rack::CaptureExceptions REDIS = Redis.new PLANS = Dhall.load("env:PLANS").sync BRAINTREE_CONFIG = Dhall.load("env:BRAINTREE_CONFIG").sync ELECTRUM = Electrum.new( **Dhall::Coder.load("env:ELECTRUM_CONFIG", transform_keys: :to_sym) ) ELECTRUM_BCH = Electrum.new( **Dhall::Coder.load("env:ELECTRON_CASH_CONFIG", transform_keys: :to_sym) ) DB = PG.connect(dbname: "jmp") DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB) DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB) class CreditCardGateway class ErrorResult < StandardError def self.for(result) if result.verification&.status == "gateway_rejected" && result.verification&.gateway_rejection_reason == "cvv" new("fieldInvalidForCvv") else new(result.message) end end end def initialize(currency, antifraud) @currency = currency @antifraud = antifraud @gateway = Braintree::Gateway.new( environment: BRAINTREE_CONFIG[:environment].to_s, merchant_id: BRAINTREE_CONFIG[:merchant_id].to_s, public_key: BRAINTREE_CONFIG[:public_key].to_s, private_key: BRAINTREE_CONFIG[:private_key].to_s ) end def merchant_account BRAINTREE_CONFIG[:merchant_accounts][@currency] end def client_token(**kwargs) kwargs[:merchant_account_id] = merchant_account.to_s if merchant_account @gateway.client_token.generate(**kwargs) end def antifraud REDIS.mget(@antifraud.map { |k| "jmp_antifraud-#{k}" }).find do |anti| anti.to_i > 2 end && Braintree::ErrorResult.new( @gateway, errors: {}, message: "Please contact support" ) end def with_antifraud result = antifraud || yield return result if result.success? @antifraud.each do |k| REDIS.incr("jmp_antifraud-#{k}") REDIS.expire("jmp_antifraud-#{k}", 60 * 60 * 24) end raise ErrorResult.for(result) end def sale(nonce, amount, **kwargs) with_antifraud do @gateway.transaction.sale( payment_method_nonce: nonce, amount: amount, merchant_account_id: merchant_account.to_s, options: { store_in_vault_on_success: true, submit_for_settlement: true }, **kwargs ) end end def customer @gateway.customer end def payment_method @gateway.payment_method end end class CardVault def self.for(gateway, nonce, amount=nil) if amount&.positive? CardDeposit.new(gateway, nonce, amount) else new(gateway, nonce) end end def initialize(gateway, nonce) @gateway = gateway @nonce = nonce end def call(auto_top_up_amount) result = vault! ThreeDSecureRepo.new.put_from_result(result) AutoTopUpRepo.new.put( @gateway.customer_id, auto_top_up_amount ) result end def vault! @gateway.default_method(@nonce) end class CardDeposit < self def initialize(gateway, nonce, amount) super(gateway, nonce) @amount = amount return unless @amount < 15 || @amount > 35 raise CreditCardGateway::ErrorResult, "amount too low or too high" end def call(*) result = super Transaction.new( @gateway.customer_id, result.transaction.id, @amount, "Credit card payment" ).save result end def vault! @gateway.sale(@nonce, @amount) end end end class JmpPay < Roda SENTRY_DSN = ENV.fetch("SENTRY_DSN", nil)&.then { |v| URI(v) } plugin :render, engine: "slim" plugin :cookies, path: "/" plugin :common_logger, $stdout plugin :content_for plugin( :assets, css: { tom_select: "tom_select.scss", loader: "loader.scss" }, js: { section_list: "section_list.js", tom_select: "tom_select.js", htmx: "htmx.js" }, add_suffix: true ) extend Forwardable def_delegators :request, :params def electrum case params["currency"] when "bch" ELECTRUM_BCH else ELECTRUM end end def nil_empty(s) s.to_s == "" ? nil : s end def stallion_shipment { to_address: { country_code: params["country-name"], postal_code: params["postal-code"], address1: nil_empty(params["street-address"]), email: nil_empty(params["email"]) }.compact, weight_unit: "g", weight: (params["esim_adapter_quantity"].to_i * 10) + (params["pcsc_quantity"].to_i * 30), size_unit: "in", length: 18, width: 8.5, height: params["pcsc_quantity"].to_i.positive? ? 1 : 0.1, package_type: "Large Envelope Or Flat", items: [ { title: "Memory Card", description: "Memory Card", sku: "003", quantity: params["esim_adapter_quantity"].to_i, value: params["currency"] == "CAD" ? 54.99 : 39.99, currency: params["currency"], country_of_origin: "CN" }, { title: "Card Reader", description: "Card Reader", sku: "002", quantity: params["pcsc_quantity"].to_i, value: params["currency"] == "CAD" ? 13.50 : 10, currency: params["currency"], country_of_origin: "CN" } ].select { |item| item[:quantity].positive? }, insured: true } end def get_rates uri = URI("https://ship.stallionexpress.ca/api/v4/rates") Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| req = Net::HTTP::Post.new(uri) req["Content-Type"] = "application/json" req["Authorization"] = "Bearer #{ENV.fetch('STALLION_TOKEN')}" body = stallion_shipment req.body = body.to_json JSON.parse(http.request(req).read_body) end end def retail_rate(rates) return unless rates&.first&.dig("total") total = BigDecimal(rates.first["total"], 2) total *= 0.75 if params["currency"] == "USD" params["country-name"] == "US" ? total.round : total.ceil end route do |r| r.on "electrum_notify" do REDIS.lpush( "exciting_#{electrum.currency}_addrs", "#{params['address']}/#{params["customer_id"]}" ) "OK" end r.assets atfd = r.cookies["atfd"] || SecureRandom.uuid one_year = 60 * 60 * 24 * 365 response.set_cookie( "atfd", value: atfd, expires: Time.now + one_year ) params.delete("atfd") if params["atfd"].to_s == "" antifrauds = [atfd, r.ip, params["atfd"]].compact.uniq r.on "esim-adapter" do r.get "braintree" do gateway = CreditCardGateway.new(params["currency"], antifrauds) if gateway.antifraud next view( :message, locals: { message: "Please contact support" } ) end render :braintree, locals: { token: gateway.client_token } end r.get "total" do next "" unless params["postal-code"].to_s != "" resp = get_rates next resp["errors"].to_a.join("
") unless resp["success"] render :total, locals: resp.merge("body" => stallion_shipment) end r.post do gateway = CreditCardGateway.new(params["currency"], antifrauds) if gateway.antifraud next view( :message, locals: { message: "Please contact support" } ) end cost = stallion_shipment[:items].map { |item| item[:quantity] * item[:value] }.sum rate = retail_rate(get_rates["rates"]) unless BigDecimal(params["amount"], 2) >= (cost + rate) raise "Invalid amount" end sale_result = gateway.sale(params["braintree_nonce"], params["amount"]) postal_lookup = JSON.parse(Net::HTTP.get(URI( "https://app.zipcodebase.com/api/v1/search?" \ "apikey=#{ENV.fetch('ZIPCODEBASE_TOKEN')}&" \ "codes=#{params['postal-code']}&" \ "country=#{params['country-name']}" ))) uri = URI("https://ship.stallionexpress.ca/api/v4/orders") Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| req = Net::HTTP::Post.new(uri) req["Content-Type"] = "application/json" req["Authorization"] = "Bearer #{ENV.fetch('STALLION_TOKEN')}" body = stallion_shipment body.merge!(body.delete(:to_address)) body[:store_id] = ENV.fetch("STALLION_STORE_ID") body[:value] = BigDecimal(params["amount"], 2) body[:currency] = params["currency"] body[:order_at] = Time.now.strftime("%Y-%m-%d %H:%m:%S") body[:order_id] = sale_result.transaction.id body[:name] = "#{params['given-name']} #{params['family-name']}" body[:city] = postal_lookup["results"].values.first.first["city"] body[:province_code] = postal_lookup["results"].values.first.first["state_code"] req.body = body.to_json resp = JSON.parse(http.request(req).read_body) view :receipt, locals: resp.merge("rate" => rate) end rescue CreditCardGateway::ErrorResult response.status = 400 $!.message end r.get do view :esim_adapter, locals: { antifraud: atfd, ip: request.ip } end end r.on :jid do |jid| Sentry.set_user(id: params["customer_id"], jid: jid) customer_id = params["customer_id"] gateway = CreditCardCustomerGateway.new(jid, customer_id, antifrauds) topup = AutoTopUpRepo.new r.on "credit_cards" do r.get do if gateway.antifraud return view( :message, locals: { message: "Please contact support" } ) end view( "credit_cards", locals: { jid: jid, token: gateway.client_token, customer_id: gateway.customer_id, antifraud: atfd, auto_top_up: topup.find(gateway.customer_id) || (gateway.payment_methods? ? "" : "15") } ) end r.post do CardVault .for( gateway, params["braintree_nonce"], params["amount"].to_d ).call(params["auto_top_up_amount"].to_i) "OK" rescue ThreeDSecureRepo::Failed gateway.remove_method($!.message) response.status = 400 "hostedFieldsFieldsInvalidError" rescue CreditCardGateway::ErrorResult response.status = 400 $!.message end end end end end run JmpPay.freeze.app