upgrades: add authorize.net integration.
Add integration for accepting payments with Authorize.net. https://developer.authorize.net/hello_world.html
This commit is contained in:
148
app/logical/authorize_net_client.rb
Normal file
148
app/logical/authorize_net_client.rb
Normal file
@@ -0,0 +1,148 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# An API client for Authorize.net. Used for processing payments for user upgrades.
|
||||
#
|
||||
# https://developer.authorize.net/api.html
|
||||
# https://developer.authorize.net/api/reference/index.html
|
||||
class AuthorizeNetClient
|
||||
class Error < StandardError; end
|
||||
|
||||
attr_reader :login_id, :transaction_key, :test_mode, :http
|
||||
|
||||
def initialize(login_id: Danbooru.config.authorize_net_login_id, transaction_key: Danbooru.config.authorize_net_transaction_key, test_mode: Danbooru.config.authorize_net_test_mode, http: Danbooru::Http.new)
|
||||
@login_id = login_id
|
||||
@transaction_key = transaction_key
|
||||
@test_mode = test_mode
|
||||
@http = http
|
||||
end
|
||||
|
||||
concerning :ApiMethods do
|
||||
def authenticate_test
|
||||
post!(
|
||||
authenticateTestRequest: {
|
||||
merchantAuthentication: {
|
||||
name: login_id,
|
||||
transactionKey: transaction_key,
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
# https://developer.authorize.net/api/reference/index.html#transaction-reporting-get-transaction-details
|
||||
def get_transaction(transaction_id)
|
||||
post!(
|
||||
getTransactionDetailsRequest: {
|
||||
merchantAuthentication: {
|
||||
name: login_id,
|
||||
transactionKey: transaction_key,
|
||||
},
|
||||
transId: transaction_id,
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
# https://developer.authorize.net/api/reference/index.html#accept-suite-get-an-accept-payment-page
|
||||
def get_hosted_payment_page(reference_id:, settings: {}, **transaction_request)
|
||||
post!(
|
||||
getHostedPaymentPageRequest: {
|
||||
merchantAuthentication: {
|
||||
name: login_id,
|
||||
transactionKey: transaction_key,
|
||||
},
|
||||
refId: reference_id,
|
||||
transactionRequest: transaction_request,
|
||||
"hostedPaymentSettings": {
|
||||
"setting": hosted_payment_settings(settings),
|
||||
},
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def hosted_payment_settings(settings)
|
||||
settings.map do |name, hash|
|
||||
{
|
||||
"settingName": "hostedPayment#{name.to_s.camelize}Options",
|
||||
"settingValue": hash.to_json,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def post!(**request)
|
||||
resp = http.post!(api_url, json: request)
|
||||
|
||||
body = resp.body.to_s.delete_prefix("\xEF\xBB\xBF") # delete UTF-8 BOM
|
||||
json = JSON.parse(body).with_indifferent_access
|
||||
|
||||
if json.dig(:messages, :resultCode) != "Ok"
|
||||
code = json.dig(:messages, :message, 0, :code)
|
||||
text = json.dig(:messages, :message, 0, :text)
|
||||
raise Error, "Authorize.net call failed (request=#{request.keys.first} code=#{code} text=#{text})"
|
||||
else
|
||||
json
|
||||
end
|
||||
end
|
||||
|
||||
# https://developer.authorize.net/api/reference/index.html#gettingstarted-section-section-header
|
||||
def api_url
|
||||
if test_mode
|
||||
"https://apitest.authorize.net/xml/v1/request.api"
|
||||
else
|
||||
"https://api.authorize.net/xml/v1/request.api"
|
||||
end
|
||||
end
|
||||
|
||||
# https://developer.authorize.net/api/reference/features/accept_hosted.html#Form_POST_URLs
|
||||
def payment_page_url
|
||||
if test_mode
|
||||
"https://test.authorize.net/payment/payment"
|
||||
else
|
||||
"https://accept.authorize.net/payment/payment"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# https://developer.authorize.net/api/reference/features/webhooks.html
|
||||
concerning :WebhookApiMethods do
|
||||
# https://developer.authorize.net/api/reference/features/webhooks.html#List_My_Webhooks
|
||||
def webhooks
|
||||
webhook_get!("webhooks")
|
||||
end
|
||||
|
||||
# https://developer.authorize.net/api/reference/features/webhooks.html#Get_a_Webhook
|
||||
def webhook(webhook_id)
|
||||
webhook_get!("webhooks/#{webhook_id}")
|
||||
end
|
||||
|
||||
# https://developer.authorize.net/api/reference/features/webhooks.html#Retrieve_Notification_History
|
||||
def notifications(status: nil)
|
||||
webhook_get!("notifications", params: { deliveryStatus: status }.compact)
|
||||
end
|
||||
|
||||
# https://developer.authorize.net/api/reference/features/webhooks.html#Retrieve_a_Specific_Notification's_History
|
||||
def notification(notification_id)
|
||||
webhook_get!("notifications/#{notification_id}")
|
||||
end
|
||||
|
||||
# https://developer.authorize.net/api/reference/features/webhooks.html#Create_A_Webhook
|
||||
def create_webhook(name:, url:, eventTypes:, status: "active")
|
||||
webhook_post!("webhooks", form: { name: name, url: url, eventTypes: eventTypes, status: status })
|
||||
end
|
||||
|
||||
def webhook_get!(path, **options)
|
||||
http.basic_auth(user: login_id, pass: transaction_key).get!(webhook_url(path), **options).parse
|
||||
end
|
||||
|
||||
def webhook_post!(path, **options)
|
||||
http.basic_auth(user: login_id, pass: transaction_key).post!(webhook_url(path), **options).parse
|
||||
end
|
||||
|
||||
# https://developer.authorize.net/api/reference/features/webhooks.html#API_Endpoint_Hosts
|
||||
def webhook_url(path)
|
||||
if test_mode
|
||||
"https://apitest.authorize.net/rest/v1/#{path}"
|
||||
else
|
||||
"https://api.authorize.net/rest/v1/#{path}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
125
app/logical/payment_transaction/authorize_net.rb
Normal file
125
app/logical/payment_transaction/authorize_net.rb
Normal file
@@ -0,0 +1,125 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# https://sandbox.authorize.net/
|
||||
# https://developer.authorize.net/hello_world.html
|
||||
# https://developer.authorize.net/api/reference/index.html
|
||||
# https://developer.authorize.net/api/reference/features/accept_hosted.html
|
||||
# https://developer.authorize.net/hello_world/testing_guide.html
|
||||
class PaymentTransaction::AuthorizeNet < PaymentTransaction
|
||||
extend Memoist
|
||||
|
||||
class InvalidWebhookError < StandardError; end
|
||||
|
||||
def create!(country: "US", allow_promotion_codes: false)
|
||||
# https://developer.authorize.net/api/reference/index.html#accept-suite-get-an-accept-payment-page
|
||||
response = api_client.get_hosted_payment_page(
|
||||
reference_id: user_upgrade.id,
|
||||
transactionType: "authCaptureTransaction",
|
||||
amount: user_upgrade.price,
|
||||
customer: {
|
||||
id: user_upgrade.purchaser.id,
|
||||
email: user_upgrade.purchaser.email_address&.address,
|
||||
},
|
||||
settings: {
|
||||
button: {
|
||||
text: "Pay",
|
||||
},
|
||||
order: {
|
||||
show: false,
|
||||
merchantName: Danbooru.config.canonical_app_name,
|
||||
},
|
||||
payment: {
|
||||
cardCodeRequired: true,
|
||||
showCreditCard: true,
|
||||
showBankAccount: false,
|
||||
},
|
||||
customer: {
|
||||
showEmail: true, requiredEmail: true, addPaymentProfile: false
|
||||
},
|
||||
billing_address: {
|
||||
show: true,
|
||||
required: false,
|
||||
},
|
||||
shipping_address: {
|
||||
show: false,
|
||||
required: false,
|
||||
},
|
||||
style: { bgColor: "blue" },
|
||||
return: {
|
||||
url: Routes.user_upgrade_url(user_upgrade),
|
||||
cancelUrl: Routes.new_user_upgrade_url(user_id: recipient.id),
|
||||
urlText: "Continue",
|
||||
cancelUrlText: "Cancel",
|
||||
showReceipt: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
[api_client.payment_page_url, response[:token]]
|
||||
end
|
||||
|
||||
def refund!(reason = nil)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
concerning :WebhookMethods do
|
||||
class_methods do
|
||||
# https://developer.authorize.net/api/reference/features/webhooks.html#Event_Types_and_Payloads
|
||||
def receive_webhook(request)
|
||||
verify_webhook!(request)
|
||||
|
||||
case request.params[:eventType]
|
||||
when "net.authorize.payment.authcapture.created"
|
||||
payment_completed(request)
|
||||
end
|
||||
end
|
||||
|
||||
# https://developer.authorize.net/api/reference/features/webhooks.html#Verifying_the_Notification
|
||||
private def verify_webhook!(request)
|
||||
payload = request.body.read
|
||||
actual_signature = request.headers["X-Anet-Signature"].to_s
|
||||
calculated_signature = "sha512=" + OpenSSL::HMAC.digest("sha512", Danbooru.config.authorize_net_signature_key, payload).unpack1("H*").upcase
|
||||
raise InvalidWebhookError unless ActiveSupport::SecurityUtils::secure_compare(actual_signature, calculated_signature)
|
||||
|
||||
request
|
||||
end
|
||||
|
||||
private def payment_completed(request)
|
||||
# Authorize.net's shitty API sends a real request with fake values when you trigger a test webhook.
|
||||
# The only way to detect a test webhook is to check for these hardcoded fake values.
|
||||
if request.params.dig(:payload, :authAmount) == 12.5 && request.params.dig(:payload, :id) == "245" && request.params.dig(:payload, :authCode) == "572"
|
||||
return
|
||||
end
|
||||
|
||||
user_upgrade_id = request.params.dig(:payload, :merchantReferenceId)
|
||||
transaction_id = request.params.dig(:payload, :id)
|
||||
user_upgrade = UserUpgrade.find(user_upgrade_id)
|
||||
user_upgrade.update!(transaction_id: transaction_id)
|
||||
user_upgrade.process_upgrade!("paid")
|
||||
end
|
||||
|
||||
private def register_webhook
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def receipt_url
|
||||
# "https://sandbox.authorize.net/ui/themes/sandbox/Transaction/TransactionReceipt.aspx?transid=#{transaction_id}" if transaction_id.present?
|
||||
end
|
||||
|
||||
def payment_url
|
||||
# "https://sandbox.authorize.net/ui/themes/sandbox/transaction/transactiondetail.aspx?transID=40092238841" if transaction_id.present?
|
||||
end
|
||||
|
||||
def transaction
|
||||
return nil if user_upgrade.transaction_id.nil?
|
||||
api_client.get_transaction(user_upgrade.transaction_id)
|
||||
end
|
||||
|
||||
def api_client
|
||||
AuthorizeNetClient.new
|
||||
end
|
||||
|
||||
memoize :api_client, :transaction
|
||||
end
|
||||
Reference in New Issue
Block a user