class Eco::API::UseCases::DefaultCases::TransferAccountCase

Public Instance Methods

main(entries, people, session, options, usecase) click to toggle source

Usecase to **actually transfer a user/account** from one person to another person in the organization.

* **invocation command**: `-transfer-account-from`

These are the steps and jobs it does:

1. **pair** person entries (note: the `destination-id` entry could not be present, it will add it in such a case).
2. **retrieve** from server persons that were not included in `people`.
3. **validation**
  - a person should only receive account from just one user
  - a person should only give account to just one user
  - every account giver should have account
4. **create jobs**
  - **move** giver's and receiver's **accounts** to dummy email addresses.
    * dummy email pattern: from name@domain.ltd --to--> demo+name.domain.ltd@ecoportal.co.nz
  - **free up** giver's and receiver's **accounts**.
  - **switch** email: set receiver's email to that of giver's dummy address.
    * to ensure account transfer, as we moved accounts to dummy emails, those dummy addresses should be used
  - **invite** receivers: adds account to the destination person in the dummy email.
    * actual user/account transfer to the person/receiver
    * **no notification** will be recived by the user, because of the dummy address at this stage
  - **restore** email: sets the receiver email from the dummy address to the final email.
    * the final email inbox will receive a **notification** of _email change_

@note

- the `csv` should contain a column `destination-id` containing the person `id` or `external_id` of the person that will be receiving the account.
- when running this case, it is recommended to use the option `-skip-batch-policies`
- it is highly recommended to either refresh the cache with `-get-people` or use the `-get-partial` option.

@param entries [Eco::API::Common::People::Entries] the input entries with the data. @param people [Eco::API::Organization::People] target existing People of the current update. @param session [Eco::API::Session] the current session where the usecase kicks in. @param options [Hash] the options that modify the case behaviour or bring some dependencies. @option options [Hash<Symbol, Object>] :include things that should be included.

* `:email` (Boolean) [false] if the `email` should be transferred as well (**command option**: `-include-email`)

@option options [Hash<Symbol, Object>] :skip things that should be excluded.

* `:api_policies` (Boolean) [false] if the `api policies` should be skipped (**command option**: `-skip-api-policies`)

@return [Void]

# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 39
def main(entries, people, session, options, usecase)
  micro    = session.micro
  move     = session.new_job("main", "move email accounts", :update, usecase, :account)
  free     = session.new_job("main", "free up accounts",    :update, usecase, :account)
  switch   = session.new_job("main", "switch email",        :update, usecase, :core)
  invite   = session.new_job("post", "invite receivers",    :update, usecase, :account)
  restore  = session.new_job("post", "restore email",       :update, usecase, :core)

  with_each_person_pair(entries, people, session, options) do |src_person, dst_person|
    # capture actual initial information
    src_doc   = src_person.account.doc
    src_email = src_person.email
    src_dummy = dummy_email(src_person)
    dst_email = dst_person.email
    dst_dummy = dummy_email(dst_person)
    copy_src_email  = options.dig(:include, :email) || !dst_person.account
    dst_end_email   = copy_src_email ? src_email : dst_email

    # account email renamings are necessary to avoid uncertainty and ensure no email taken error
    move.add(dst_person)    {|dst| dst.email = dst_dummy}
    move.add(src_person)    {|src| src.email = src_dummy}
    # free accounts up!
    free.add([dst_person, src_person]) {|person| person.account = nil}
    # to effectivelly transfer the user/account, email should be the same during invite
    # otherwise the account doesn't actually get transferred but just copied
    switch.add(dst_person)  {|dst| dst.email = src_dummy}
    # do the actual transfer of account
    invite.add(dst_person)  {|dst| dst.account = src_doc}
    # recover the original email, if the receiver had account
    restore.add(dst_person) {|dst| dst.email = dst_end_email}
  end.tap do |units|
    if options[:simulate]
      units.each {|unit| puts unit.persons.map(&:external_id).join(" --> ")}
    end
  end
end

Private Instance Methods

<=>(other) click to toggle source

givers go first

# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 180
def <=>(other)
  return -1 if src_person == other.dst_person
  return  1 if dst_person == other.src_person
  0
end
decouple_peer(entry) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 152
def decouple_peer(entry)
  entry.new({
    "id"          => entry_peer_id(entry),
    "external_id" => entry_peer_id(entry),
    "email"       => entry_peer_id(entry),
    "idx"         => entry.idx
  })
end
dummy_email(person) click to toggle source

if the person has account and an email different to demo+__@ecoportal.co.nz it transforms email to demo+__@ecoportal.co.nz

# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 80
def dummy_email(person)
  return nil   unless email = person.email
  return email if email.start_with?("demo") && email.end_with?("@ecoportal.co.nz")
  return email unless person.account
  "demo+#{email.split("@").join(".")}@ecoportal.co.nz"
end
entry_peer(entry, entries) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 147
def entry_peer(entry, entries)
  return nil unless peer_id = entry_peer_id(entry)
  entries.entry(id: peer_id, external_id: peer_id) || decouple_peer(entry)
end
entry_peer_id(entry) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 161
def entry_peer_id(entry)
  dest_id = entry.final_entry["destination-id"]
  return dest_id unless dest_id.to_s.strip.empty?
end
entry_peer_id?(entry) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 166
def entry_peer_id?(entry)
  entry.final_entry.key?("destination-id")
end
expect_destination_id!(entries, logger) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 251
def expect_destination_id!(entries, logger)
  unless entries.length > 0
    logger.error("Your csv is empty")
    exit(1)
  end
  unless entry_peer_id?(entries.first)
    logger.error("You haven't defined a column 'destination-id' to whom the account should be transferred")
    exit(1)
  end
end
find_repeated(units, &block) click to toggle source

Units helpers

# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 193
def find_repeated(units, &block)
  units.group_by(&block).select do |k,v|
    v.count > 1
  end.map {|k, v| v.first}
end
new_unit(e1, e2, p1, p2) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 188
def new_unit(e1, e2, p1, p2)
  unit_type.new(e1, e2, p1, p2)
end
paired_entries(entries, session) { |missing_peer| ... } click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 133
def paired_entries(entries, session)
  expect_destination_id!(entries, session.logger)
  missing_peer = []
  entries.each_with_object([]) do |source, out|
    if peer = entry_peer(source, entries)
      out.push([source, peer])
    else
      missing_peer.push(source)
    end
  end.tap do |paired_entries|
    yield(missing_peer) if block_given?
  end
end
persons() click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 173
def persons
  [src_person, dst_person]
end
report_missing_account(str, logger) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 262
def report_missing_account(str, logger)
  logger.error("The following source people do not have account:\n#{str}")
end
report_missing_peer!(unpaired_entries, logger) click to toggle source

Messaging & Validation

# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 233
def report_missing_peer!(unpaired_entries, logger)
  unless unpaired_entries.empty?
    msg  = "The following rows are missing 'destination-id':\n\n"
    msg += unpaired_entries.map {|entry| str_missing_peer(entry)}.join("\n")
    logger.error(msg)
    exit(1)
  end
end
report_repeated(str, logger, msg = "The following entries are repeated") click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 266
def report_repeated(str, logger, msg = "The following entries are repeated")
  logger.error("#{msg}\n#{str}")
end
reversed_unit?(u2) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 176
def reversed_unit?(u2)
  (src_person == u2.dst_person) && (dst_person == u2.src_person)
end
sort_units(units) click to toggle source

account givers that are receivers should give first exchangers are placed at the beginning

# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 105
def sort_units(units)
  base       = units.dup
  # cases where two persons exchange account
  exchangers = base.select do |source|
    base.any? {|dest| source.reversed_unit?(dest)}
  end
  exchangers | base.sort
end
str_missing_peer(entry) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 242
def str_missing_peer(entry)
  "Source #{entry.to_s(:identify)} is missing 'destination-id'."
end
str_person_entry(person, entry, row = nil) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 246
def str_person_entry(person, entry, row = nil)
  str_row = row ? "(actual row: #{row}) " : nil
  "#{str_row}id: '#{person.id || person.external_id}' email:#{person.email} -> Entry #{entry.to_s(:identify)}"
end
to_persons(paired_entry, people, session, options) { |*persons| ... } click to toggle source

Entry helpers

# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 115
def to_persons(paired_entry, people, session, options)
  micro    = session.micro
  [].tap do |persons|
    micro.with_each(paired_entry, people, options) do |entry, person|
      next persons.push(person) unless person.new?
      if person = session.batch.search([entry], silent: true).people.first
        persons.push(person)
        people << person
        next
      end
      session.logger.error("This person does not exist: #{entry.to_s(:identify)}")
      exit(1)
    end
  end.tap do |persons|
    yield(*persons) if block_given?
  end
end
uniq_array(ary, &block) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 199
def uniq_array(ary, &block)
  ary.each_with_object([]) do |e, uniq|
    uniq.push(e) unless uniq.any? {|chosen| block.call(chosen, e)}
  end
end
unit_type() click to toggle source

Unit type helpers

# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 171
def unit_type
  @unit_type ||= Struct.new(:src_entry, :dst_entry, :src_person, :dst_person) do
    def persons
      [src_person, dst_person]
    end
    def reversed_unit?(u2)
      (src_person == u2.dst_person) && (dst_person == u2.src_person)
    end
    # givers go first
    def <=>(other)
      return -1 if src_person == other.dst_person
      return  1 if dst_person == other.src_person
      0
    end
  end
end
validate_pairs!(units, logger) click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 205
def validate_pairs!(units, logger)
  missing_account = []
  src_repeated = find_repeated(units) do |unit|
    unit.src_person.tap do |src_person|
      missing_account << (src_person.account ? nil : str_person_entry(unit.src_person, unit.src_entry))
    end
  end.each_with_object([]) do |unit, lines|
    lines << str_person_entry(unit.src_person, unit.src_entry)
  end
  dst_repeated = find_repeated(units) do
    |unit| unit.dst_person
  end.each_with_object([]) do |unit, lines|
    lines << str_person_entry(unit.dst_person, unit.dst_entry, unit.src_entry.idx)
  end
  missing_account.compact!
  unless [missing_account, src_repeated, dst_repeated].all?(&:empty?)
    report_missing_account(missing_account.join("\n"), logger) unless missing_account.empty?

    msg = Proc.new do |spot|
      "Transfers should be a 1.to.1 relation. The following #{spot} entries are repeated in the csv:"
    end
    report_repeated(src_repeated.join("\n"), logger, msg["SOURCE"])       unless src_repeated.empty?
    report_repeated(dst_repeated.join("\n"), logger, msg["DESTINATION"])  unless dst_repeated.empty?
    exit(1)
  end
end
with_each_person_pair(entries, people, session, options) { |*persons| ... } click to toggle source
# File lib/eco/api/usecases/default_cases/transfer_account_case.rb, line 87
def with_each_person_pair(entries, people, session, options)
  units = paired_entries(entries, session) do |unpaired_entries|
    report_missing_peer!(unpaired_entries, session.logger)
  end.map do |entry_pair|
    person_pair = to_persons(entry_pair, people, session, options)
    new_unit(*entry_pair, *person_pair)
  end
  sort_units(units).tap do |units|
    # check that there are no repeated sources or destinations, that sources have account
    validate_pairs!(units, session.logger)
    units.each do |unit|
      yield(*unit.persons) if block_given?
    end
  end
end