Export User Data As CSV and Send An Email with Ruby on Rails
Tags: rails, ruby
Published: 2022-09-17 02:51:22

Sometimes we need to export user data we have in our app to comply with GDPR. So here I will explain how to export user data as a CSV file and attach it to an email or just upload it to ActiveStorage and S3.

In this example code, I have an app where users save their financial transactions with bank accounts’ names, currencies, and transaction dates.

We will create a temp file in the rails temp directory and then send it to SendGrid for email or storage. We will add the data to a temporary CSV file then call `export_to_storage` to save it to ActiveStorage to call `send_email` to send the user email with the attached CSV file.

In [this article](https://sulmanweb.com/send-emails-using-sendgrid-sdk-for-ruby-on-rails-using-dynamic-sendgrid-email-templates/ "Send Emails using SendGrid SDK for Ruby on Rails using Dynamic SendGrid Email Templates"), I have already explained how to send emails with attachments using SendGrid.

`app/services/export_services/export_transaction_service.rb`
```ruby
# frozen_string_literal: true

# app/services/export_services/export_transaction_service.rb
# Example:
#   ExportServices::ExportTransactionService.new({ user: User.last}).call
#   ExportServices::ExportTransactionService.new({ user: User.last, from_date: '2022-01-01', to_date: '2022-09-01'}).call
module ExportServices
  # ExportTransactionService
  class ExportTransactionService
    # initialize the service with user, from_date and to_date and setting transactions accordingly
    def initialize(params) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
      @user = params[:user]
      @from_date = params[:from_date] ? params[:from_date].to_date : 10.years.ago # -Infinity
      @to_date = params[:to_date] ? params[:to_date].to_date : Time.zone.now
      @transactions = @user.transactions.where(transaction_date: @from_date..@to_date)
                           .joins(:account, :category).kept.order(transaction_date: :desc)
    end

    # service work when call method is called
    def call
      return if @transactions.length.zero?

      generate_csv_file
      # export_to_storage
      send_email
    end

    # This method creates a temp CSV file and fills data based on initialized transactions
    def generate_csv_file # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
      @tempfile = Tempfile.new(["export_#{Time.zone.now.strftime('%Y%m%d%H%M%S')}_#{@user.id}", '.csv']).tap do |file|
        CSV.open(file, 'wb') do |csv|
          # CSV Header Row
          csv << %w[Date Description Type Amount Currency Category Account]

          # CSV Rows, each row representing a transaction
          @transactions.find_each do |transaction|
            csv << [
              transaction.transaction_date.to_date.to_s,
              transaction.description,
              transaction.transaction_type == 'income' ? 'Income' : 'Expense',
              transaction.value.to_f.round(2),
              transaction.account.currency.code,
              transaction.category.name,
              transaction.account.name,
            ]
          end
        end
      end
    end

    # Thios method adds the tempfile to ActiveStorage and hence to S3 service if attached
    # I have attached s3 to production environment
    # returns url of the uploaded CSV File
    def export_to_storage # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
      # Document Model `has_attached_file :doc` and `user has_many :documents` and `document belongs_to :user`
      document = Document.new(user: @user)
      document.doc.attach(
        io: @tempfile,
        filename: @tempfile.path.split('/').last,
        content_type: 'application/csv'
      )
      document.save!
      @file_url = if Rails.env.production?
                    # returns s3 public url
                    Rails.application.routes.url_helpers.rails_public_blob_url(@document.doc)
                  else
                    # return local url
                    Rails.application.routes.url_helpers.rails_blob_url(@document.doc)
                  end
    end

    # This method sends email with temp file attached
    # https://gist.github.com/sulmanweb/eab697301cf8209572c5a95af6543b3a
    # https://sulmanweb.com/send-emails-using-sendgrid-sdk-for-ruby-on-rails-using-dynamic-sendgrid-email-templates/
    def send_email # rubocop:disable Metrics/MethodLength
      EmailJob.new.perform(
        @user.id,
        { name: @user.name },
        ENV.fetch('EXPORT_TEMPLATE', nil),
        [
          {
            file: @tempfile.path,
            type: 'application/csv',
            name: @tempfile.path.split('/').last,
            content_id: 'export_file'
          }
        ]
      )
    end
  end
end
```
[GitHub Gist](https://gist.github.com/sulmanweb/a3407dcc9cd7fddb42e251055d20f247)

In Line 11, we initialize the service with params, required is the user, and optional are date ranges. After initialization, we will get the provided user’s transactions.

The `call` method on Line 20 checks whether the user has at least one transaction, then it calls `generate_csv_file` which creates a temp CSV file with the user’s transactions and then uses `send_email` to send that email with the attached file.

The method `export_to_storage` on line 54 is optional if we want to upload to s3. In that case, this method will return the URL of s3 or the local storage of the file.

Examples to call this service in your code be like:
```ruby
ExportServices::ExportTransactionService.new({ user: User.last}).call
```

```ruby
ExportServices::ExportTransactionService.new({ user: User.last, from_date: '2022-01-01', to_date: '2022-09-01'}).call
```

Feel free to ask more questions in the comment section.

---- 

Happy Coding!