-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5b223ee
commit 6c3a107
Showing
5 changed files
with
193 additions
and
116 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
require "zip" | ||
|
||
class PaymentHistory::CsvImporter | ||
attr_accessor :user, :calculate_from_date, :temp_files | ||
|
||
CSV_READER_OPTIONS = { | ||
converters: :all, | ||
header_converters: :symbol | ||
}.freeze | ||
|
||
SAVE_EVERY_N_ROWS = 1000 | ||
|
||
UNKNOWN_APP_TITLE = "Unknown".freeze | ||
|
||
CSV_REVENUE_TYPES = { | ||
"recurring_revenue" => [ | ||
"RecurringApplicationFee", | ||
"Recurring application fee", | ||
"App sale – recurring", | ||
"App sale – subscription", | ||
"App sale – 30-day subscription", | ||
"App sale – yearly subscription" | ||
], | ||
"onetime_revenue" => [ | ||
"OneTimeApplicationFee", | ||
"ThemePurchaseFee", | ||
"One time application fee", | ||
"Theme purchase fee", | ||
"App sale – one-time", | ||
"App sale – usage", | ||
"Service sale" | ||
], | ||
"affiliate_revenue" => [ | ||
"AffiliateFee", | ||
"Affiliate fee", | ||
"Development store referral commission", | ||
"Affiliate referral commission", | ||
"Shopify Plus referral commission" | ||
], | ||
"refund" => [ | ||
"Manual", | ||
"ApplicationDowngradeAdjustment", | ||
"ApplicationCredit", | ||
"AffiliateFeeRefundAdjustment", | ||
"Application credit", | ||
"Application downgrade adjustment", | ||
"Application fee refund adjustment", | ||
"App credit", | ||
"App refund", | ||
"App credit refund", | ||
"Development store commission adjustment", | ||
"Payout correction", | ||
"App downgrade", | ||
"Service refund" | ||
], | ||
"usage_revenue" => [ | ||
"App sale – usage", | ||
"Usage application fee", | ||
"AppUsageSale" | ||
] | ||
}.freeze | ||
|
||
def initialize(user:, filename:) | ||
@user = user | ||
@calculate_from_date = user.calculate_from_date | ||
@temp_files = {} | ||
@temp_files[:csv] = prepare_csv_file(filename) | ||
@rows_processed_count = 0 | ||
@batch_of_payments = [] | ||
end | ||
|
||
def import! | ||
clear_old_payments | ||
import_new_payments | ||
rescue => e | ||
handle_import_error(e) | ||
ensure | ||
close_and_unlink_temp_files | ||
end | ||
|
||
private | ||
|
||
def clear_old_payments | ||
user.payment_histories.where("payment_date > ?", calculate_from_date).delete_all | ||
end | ||
|
||
def import_new_payments | ||
# Loops through CSV file, saving in chunks of N rows | ||
CsvHashReader.foreach(@temp_files[:csv], CSV_READER_OPTIONS) do |csv_row| | ||
next if irrelevant_row?(csv_row) | ||
break if row_too_old?(csv_row) | ||
|
||
@batch_of_payments << new_payment(csv_row) | ||
|
||
@rows_processed_count += 1 | ||
if @rows_processed_count % SAVE_EVERY_N_ROWS == 0 | ||
save_chunk(@batch_of_payments) | ||
user.update(import: "Importing (#{@rows_processed_count} rows processed)", import_status: 100) | ||
end | ||
end | ||
# Save any remaining rows | ||
save_chunk(@batch_of_payments) | ||
true | ||
end | ||
|
||
def new_payment(csv_row) | ||
user.payment_histories.new( | ||
app_title: csv_row[:app_title].presence || UNKNOWN_APP_TITLE, | ||
charge_type: calculate_charge_type(csv_row), | ||
shop: csv_row[:shop], | ||
shop_country: csv_row[:shop_country], | ||
payment_date: csv_row[:charge_creation_time], | ||
revenue: csv_row[:partner_share].to_f | ||
) | ||
end | ||
|
||
def save_chunk(payments) | ||
# Uses "activerecord-import", which is much faster than saving each row individually | ||
PaymentHistory.import(payments, validate: false, no_returning: true) if payments.present? | ||
@batch_of_payments = [] | ||
end | ||
|
||
def irrelevant_row?(csv_row) | ||
csv_row[:charge_creation_time].blank? || csv_row[:partner_share].to_f == 0.0 | ||
end | ||
|
||
def row_too_old?(csv_row) | ||
csv_row[:charge_creation_time] < calculate_from_date.to_s | ||
end | ||
|
||
def calculate_charge_type(csv_row) | ||
charge_type = CSV_REVENUE_TYPES.find { |_key, value| value.include?(csv_row[:charge_type]) }&.first | ||
if charge_type == "usage_revenue" | ||
charge_type = user.count_usage_charges_as_recurring ? "recurring_revenue" : "onetime_revenue" | ||
end | ||
charge_type | ||
end | ||
|
||
def prepare_csv_file(filename) | ||
file = fetch_from_s3(filename) | ||
if zipped?(filename) | ||
extract_zip_file(file) | ||
else | ||
file | ||
end | ||
end | ||
|
||
def zipped?(filename) | ||
filename.include?(".zip") | ||
end | ||
|
||
def fetch_from_s3(filename) | ||
temp_files[:s3_download] = Tempfile.new("s3_download") | ||
s3 = Aws::S3::Client.new | ||
s3.get_object({bucket: "partner-metrics", key: filename}, target: temp_files[:s3_download].path) | ||
temp_files[:s3_download] | ||
end | ||
|
||
def extract_zip_file(zipped_file) | ||
temp_files[:unzipped] = Tempfile.new("unzipped") | ||
Zip.on_exists_proc = true | ||
Zip.continue_on_exists_proc = true | ||
Zip::File.open(zipped_file.path) do |zip_file| | ||
zip_file.each do |entry| | ||
entry.extract(temp_files[:unzipped]) | ||
end | ||
end | ||
temp_files[:unzipped] | ||
end | ||
|
||
def handle_import_error(e) | ||
# TODO: Create a generic import status class | ||
user.update(import: "Failed", import_status: 100, partner_api_errors: "Error: #{e.message}") | ||
# Resque swallows errors, so we need to log them here | ||
Rails.logger.error("Error importing CSV: #{e.message}") | ||
Rails.logger.error(e.backtrace.join("\n")) | ||
raise e | ||
end | ||
|
||
def close_and_unlink_temp_files | ||
temp_files.each_value(&:close) | ||
temp_files.each_value(&:unlink) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
app/workers/import_worker_metrics.rb → app/workers/import_metrics_worker.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
# TODO: Convert to ActiveJob -> Sidekiq | ||
|
||
class ImportMetricsWorker | ||
@queue = :import_queue | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters