Created
December 18, 2024 23:32
-
-
Save heyitsjames/c0afaba4059cfe7ea4309f0d50c72cec to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| defmodule Dozer.Integrations.Yardi do | |
| @moduledoc """ | |
| Yardi is a 3rd party app that many of our customers use, and they do lots of things related to | |
| vendor payments and property management. | |
| The main purpose for this integration is that while our mutual customers use Rabbet to track and | |
| approve documents for payment, we don't have any way to actually initiate the money transfers | |
| and tie that back to balance sheets etc. So we offer this and other similar integrations (Avid, Nexus) | |
| to allow the customers to pay out balances and tie into their accounting systems. | |
| We send them our Invoices / Pay Apps as what they call Payables. Within that, each Line Item | |
| from our side is a PayableDetail on theirs. | |
| Future improvements include: | |
| * Pulling / syncing our Vendors list with Yardi, as this is typically the System of Record for our | |
| mutual customers | |
| * 2-way syncing of Invoices with Yardi. We will need to poll periodically and pull in new changes: | |
| new or updated Invoices | |
| For more high-level information about the Yardi integration, check out this great deck that our Revenue | |
| team uses: https://docs.google.com/presentation/d/1Qh78IrzazILEb5i_Kbv2-rVMpZ0di0nZWUlPsclsXkY/edit#slide=id.gfe77c39f55_0_5 | |
| """ | |
| require Logger | |
| use Dozer.Support.Utilities | |
| import Ecto.Query | |
| import Dozer.Support.Utilities.ToMoney | |
| import Dozer.Support.Utilities.FormatMoney | |
| import Dozer.Support.Utilities.FormatDate | |
| import Dozer.Support.Utilities.SanitizeAddressState | |
| alias Ecto.UUID | |
| alias Dozer.Accounts | |
| alias Dozer.Accounts.Organization | |
| alias Dozer.Accounts.Permissions | |
| alias Dozer.Accounts.Vendor | |
| alias Dozer.Accounts.VendorRelationship | |
| alias Dozer.CostCodes.JobCostCode | |
| alias Dozer.Documentation.Document | |
| alias Dozer.DocumentReviews | |
| alias Dozer.DrawRequest.BudgetLineItem | |
| alias Dozer.Integrations.VendorSync | |
| alias Dozer.Integrations.Yardi.Api | |
| alias Dozer.Integrations.Yardi.Api.AuthParams | |
| alias Dozer.Integrations.Yardi.Api.Custom | |
| alias Dozer.Integrations.Yardi.Api.Payable | |
| alias Dozer.Integrations.Yardi.Api.PayableDetail | |
| alias Dozer.Integrations.Yardi.Workers.YardiJournalEntryWorker | |
| alias Dozer.Integrations.Yardi.Workers.YardiPayableWorker | |
| alias Dozer.Integrations.Yardi.Workers.YardiVendorWorker | |
| alias Dozer.Invoices | |
| alias Dozer.Invoices.InvoiceLineItemJobCostCodeSchema | |
| alias Dozer.Invoices.InvoiceLineItemSchema | |
| alias Dozer.Invoices.InvoiceSchema | |
| alias Dozer.Invoices.NewInvoice | |
| alias Dozer.Invoices.UpdateInvoice | |
| alias Dozer.Origination | |
| alias Dozer.Origination.Project | |
| alias Dozer.Repo | |
| alias Dozer.VendorLineItems | |
| @type uuid :: Ecto.UUID.t() | |
| @type invoice :: Ecto.Schema.t() | |
| @vendor_invoicing_endpoint "/webservices/ItfVendorInvoicing.asmx" | |
| @api_rate_limit :timer.seconds(1) | |
| @server_tz "America/New_York" | |
| @doc """ | |
| Send (create) invoices through the Yardi api. | |
| """ | |
| @spec send_invoices(project_id :: uuid, document_ids :: [uuid]) :: | |
| {:error, any} | {:ok, [invoice]} | |
| def send_invoices(project_id, document_ids) do | |
| project = get_project(project_id) | |
| invoices = get_invoices(document_ids) | |
| auth_params = auth_params(project.organization) | |
| url = project.organization.yardi_url <> @vendor_invoicing_endpoint | |
| payables = Enum.map(invoices, &map_payable(project, &1, project.organization)) | |
| uuid = Ecto.UUID.generate() | |
| Logger.info(%{ | |
| message: "[YARDI] Yardi.send_invoices", | |
| project_id: project_id, | |
| document_ids: document_ids, | |
| batch_description_id: uuid | |
| }) | |
| with {:ok, batch_id} <- | |
| Api.open_payable_batch(url, auth_params, "Rabbet API #{uuid}", project.custom_id), | |
| {:ok, _response} <- Api.add_payables_to_batch(url, auth_params, batch_id, payables), | |
| {:ok, _response} <- Api.post_payable_batch(url, auth_params, batch_id) do | |
| {:ok, invoices} | |
| else | |
| {:error, errors} -> {:error, {invoices, errors}} | |
| end | |
| end | |
| def invoice_sync(organization_id) when is_binary(organization_id) do | |
| query = | |
| from project in Project, | |
| where: project.organization_id == ^organization_id, | |
| where: project.setup_complete, | |
| preload: [:organization] | |
| # Yardi strongly discourages simultaneous API calls to the same endpoint | |
| # so we need to execute sequentially | |
| query | |
| |> Repo.all() | |
| |> Enum.each(&pull_data_from_yardi/1) | |
| {:ok, Repo.get!(Organization, organization_id)} | |
| end | |
| def invoice_sync(%Organization{id: organization_id}) do | |
| invoice_sync(organization_id) | |
| end | |
| def vendor_sync(%Organization{} = organization) do | |
| auth_params = auth_params(organization) | |
| url = organization.yardi_url <> @vendor_invoicing_endpoint | |
| VendorSync.publish_subscription(organization, "Retrieving vendors...") | |
| case Api.get_vendors(url, auth_params) do | |
| {:ok, vendors} -> | |
| vendors = Enum.reject(vendors, &Map.get(&1, "is_inactive", false)) | |
| VendorSync.publish_subscription(organization, "#{length(vendors)} vendors found") | |
| vendors | |
| |> Enum.map(fn vendor -> | |
| YardiVendorWorker.new(%{organization_id: organization.id, vendor: vendor}) | |
| end) | |
| |> Oban.insert_all() | |
| {:error, error_message} -> | |
| VendorSync.publish_subscription(organization, "Error: #{error_message}") | |
| if error_message !== organization.yardi_vendors_sync_error_message do | |
| update_organization_yardi_error_details(organization, error_message) | |
| end | |
| end | |
| {:ok, organization} | |
| end | |
| def vendor_sync(organization_id) do | |
| organization = Repo.get!(Organization, organization_id) | |
| vendor_sync(organization) | |
| end | |
| @spec generate_vendor_from_yardi(String.t(), map()) :: | |
| {:ok, Organization.t()} | {:error, Ecto.Changeset.t()} | |
| def generate_vendor_from_yardi(organization_id, unformatted_vendor) do | |
| customer = Repo.get!(Organization, organization_id) | |
| vendor = atomize_keys(unformatted_vendor) | |
| attrs = %{ | |
| city: vendor.city, | |
| email_addresses: [vendor.email], | |
| is_1099: vendor.it_gets_1099, | |
| name: get_vendor_name(vendor), | |
| phone_numbers: get_vendor_phone_numbers(vendor), | |
| state: if(vendor.state == "", do: nil, else: String.downcase(vendor.state)), | |
| street_address: vendor.address_1, | |
| vendor_cost_code: vendor.vendor_id, | |
| zip: vendor.zip_code, | |
| vendor_source: "yardi" | |
| } | |
| query = | |
| from vendor in Vendor, | |
| join: vendor_relationship in VendorRelationship, | |
| on: vendor_relationship.vendor_id == vendor.id, | |
| where: vendor_relationship.customer_id == ^customer.id, | |
| where: vendor.vendor_cost_code == ^vendor.vendor_id, | |
| where: not vendor.is_deleted, | |
| order_by: [desc: vendor.inserted_at], | |
| limit: 1 | |
| result = | |
| query | |
| |> Repo.one() | |
| |> case do | |
| nil -> | |
| VendorSync.publish_subscription( | |
| customer, | |
| "Creating new vendor: #{attrs.name}" | |
| ) | |
| Accounts.create_vendor(customer, attrs) | |
| vendor_to_update -> | |
| VendorSync.publish_subscription( | |
| customer, | |
| "Updating found vendor: #{attrs.name}" | |
| ) | |
| Accounts.update_vendor(vendor_to_update, attrs) | |
| end | |
| set_organization_last_synced_at_date(customer) | |
| result | |
| end | |
| def pull_data_from_yardi(project_id) do | |
| with {:ok, payables} <- fetch_yardi_payables_for_project(project_id), | |
| {:ok, journal_entries} <- fetch_yardi_journal_entries_for_project(project_id) do | |
| enqueue_payables(project_id, payables) | |
| enqueue_journal_entries(project_id, journal_entries) | |
| {:ok, project_id} | |
| else | |
| {:error, error_message} -> {:error, error_message} | |
| end | |
| end | |
| def fetch_yardi_journal_entries_for_project(project_id) do | |
| project = get_project(project_id) | |
| auth_params = auth_params(project.organization) | |
| url = project.organization.yardi_url <> @vendor_invoicing_endpoint | |
| Api.get_journal_entries_from_yardi(url, auth_params, project.custom_id) | |
| end | |
| def fetch_yardi_payables_for_project(project_id) do | |
| project = get_project(project_id) | |
| auth_params = auth_params(project.organization) | |
| url = project.organization.yardi_url <> @vendor_invoicing_endpoint | |
| {from_date, to_date} = | |
| get_from_date_and_to_date( | |
| project.accounts_payable_last_synced_at, | |
| project.organization.accounts_payable_settings | |
| ) | |
| result_one = Api.get_payables_from_yardi(url, auth_params, project, from_date, to_date) | |
| result_two = | |
| Api.get_payables_from_yardi_by_last_modified_date( | |
| url, | |
| auth_params, | |
| project, | |
| from_date, | |
| to_date | |
| ) | |
| # we have to make two payable calls because the two calls by themselves | |
| # don't return all of the expected payables | |
| with {:ok, payables_one} <- result_one, | |
| {:ok, payables_two} <- result_two do | |
| {:ok, payables_one ++ payables_two} | |
| else | |
| {:error, error_message} -> | |
| if error_message !== project.accounts_payable_sync_error_message do | |
| update_project_accounts_payable_error_details(project, error_message) | |
| end | |
| Logger.error("Failed to retrieve Yardi payables #{inspect(error_message)}") | |
| {:error, error_message} | |
| end | |
| end | |
| def generate_invoice_from_yardi_payable(project_id, unformatted_payable) do | |
| # We hit Yardi and they give us `payables`. | |
| # For each Payable, we format the data into a new Invoice schema record. | |
| # We use that new Invoice record to call the Yardi API to get document images. | |
| # If we don't get a document image for that Invoice, we generate it ourselves | |
| # and create two Document schema records: | |
| # One with type null | |
| # One with type `invoice` and the correlation_id set to the previous Document record. | |
| # we also associate this type `invoice` record to the Invoice record via the invoice.document_id | |
| # foreign key. | |
| project = get_project(project_id) | |
| formatted_payable_details = | |
| unformatted_payable | |
| |> Map.get("details", []) | |
| |> Enum.map(&atomize_keys/1) | |
| payable = | |
| unformatted_payable | |
| |> atomize_keys() | |
| |> Map.put( | |
| :details, | |
| formatted_payable_details | |
| ) | |
| vendor = | |
| case get_vendor_using_yardi_person_id(payable.person_id, project.organization_id) do | |
| nil -> fetch_and_create_vendor_by_yardi_id(project.organization, payable.person_id) | |
| known_vendor -> known_vendor | |
| end | |
| auth_params = auth_params(project.organization) | |
| url = project.organization.yardi_url <> @vendor_invoicing_endpoint | |
| corrected_payable = fix_payable_details(payable, project.custom_id) | |
| case get_existing_invoice(payable, project_id) do | |
| nil -> | |
| new_invoice = payable_to_new_invoice(corrected_payable, project, vendor) | |
| {:ok, %{storage_path: file_token, content_type: content_type, page_count: page_count}} = | |
| get_file_token_for_payable( | |
| new_invoice, | |
| project, | |
| project.organization, | |
| corrected_payable, | |
| url, | |
| auth_params | |
| ) | |
| # Order of schema creation MUST be UploadDocument, InvoiceDocument, Invoice. | |
| Repo.transaction(fn -> | |
| {:ok, upload_document} = | |
| add_upload_document( | |
| project, | |
| vendor, | |
| new_invoice, | |
| corrected_payable, | |
| "payable", | |
| file_token, | |
| content_type, | |
| page_count | |
| ) | |
| {:ok, invoice_document} = add_invoice_document(new_invoice, upload_document) | |
| {:ok, invoice} = insert_new_invoice(new_invoice, invoice_document) | |
| attach_invoice_documentation(invoice, invoice_document) | |
| set_project_last_synced_at_date(project) | |
| invoice | |
| end) | |
| document_to_update -> | |
| if is_nil(document_to_update.draw) or document_to_update.draw.state != "funded" do | |
| update_existing_payable(document_to_update, corrected_payable, vendor, project) | |
| else | |
| :ok | |
| end | |
| end | |
| end | |
| @doc """ | |
| For each Journal Entry, an invoice is created. | |
| If the organization enables the :yardi_pull_individualize_journal_entries setting, each | |
| journal entry is split based on the line item category_id. | |
| The new Invoice record is used to call the Yardi API to get document images. | |
| If a document image is not returned for that Invoice, a document is generated from a default template. | |
| Additionally: | |
| Two %Document{} records are created: | |
| - One with type null | |
| - One with type `invoice` and the correlation_id set to the previous Document record. | |
| We also associate this type `invoice` record to the Invoice record via the invoice.document_id foreign key. | |
| """ | |
| def generate_invoice_from_yardi_journal_entry(project_id, journal_entry) do | |
| project = get_project(project_id) | |
| details = | |
| journal_entry | |
| |> Map.get("details", []) | |
| |> Enum.map(&atomize_keys/1) | |
| atomized_journal_entry = | |
| journal_entry | |
| |> atomize_keys() | |
| |> Map.put(:details, details) | |
| individualize_journal_entries? = | |
| Permissions.is_permitted?(project.organization, :yardi_pull_individualize_journal_entries) | |
| if individualize_journal_entries? do | |
| generate_split_invoices(project, atomized_journal_entry) | |
| else | |
| generate_single_invoices(project, atomized_journal_entry) | |
| end | |
| end | |
| def update_project_accounts_payable_error_details(project, error) do | |
| attrs = %{ | |
| accounts_payable_sync_failing_since: Timex.now(), | |
| accounts_payable_sync_error_message: error | |
| } | |
| project | |
| |> Project.changeset(attrs) | |
| |> Repo.update() | |
| end | |
| def update_organization_yardi_error_details(organization, error) do | |
| attrs = %{ | |
| yardi_vendors_sync_failing_since: Timex.now(), | |
| yardi_vendors_sync_error_message: error | |
| } | |
| organization | |
| |> Organization.update_changeset(attrs) | |
| |> Repo.update() | |
| end | |
| def get_vendor_using_yardi_person_id(person_id, customer_id) do | |
| query = | |
| from vendor in Vendor, | |
| join: vendor_relationship in VendorRelationship, | |
| on: vendor_relationship.vendor_id == vendor.id, | |
| where: vendor_relationship.customer_id == ^customer_id, | |
| where: vendor.vendor_cost_code == ^person_id, | |
| limit: 1 | |
| Repo.one(query) | |
| end | |
| defp generate_split_invoices(project, journal_entry) do | |
| filtered_details = filter_details(journal_entry.details, project.custom_id) | |
| split_journal_entries = split_journal_entry_by_details(journal_entry, filtered_details) | |
| Enum.each(split_journal_entries, fn split_journal_entry -> | |
| create_invoice_from_journal_entry(split_journal_entry, project) | |
| end) | |
| end | |
| defp generate_single_invoices(project, journal_entry) do | |
| filtered_details = filter_details(journal_entry.details, project.custom_id) | |
| total_amount = aggregate_journal_entry_amounts(filtered_details) | |
| corrected_journal_entry = | |
| journal_entry | |
| |> Map.put(:details, filtered_details) | |
| |> Map.put(:total_amount, total_amount) | |
| entry_has_details? = not Enum.empty?(corrected_journal_entry.details) | |
| if entry_has_details? do | |
| create_invoice_from_journal_entry(corrected_journal_entry, project) | |
| end | |
| end | |
| defp create_invoice_from_journal_entry(journal_entry, project) do | |
| case get_existing_invoice(journal_entry, project.id) do | |
| nil -> | |
| new_invoice = journal_entry_to_new_invoice(journal_entry, project, project.organization) | |
| {:ok, %{storage_path: file_token}} = | |
| create_pdf(new_invoice, project, project.organization, "journal-entry-template") | |
| # Order of schema creation MUST be UploadDocument, InvoiceDocument, Invoice. | |
| Repo.transaction(fn -> | |
| {:ok, upload_document} = | |
| add_upload_document( | |
| project, | |
| project.organization, | |
| new_invoice, | |
| journal_entry, | |
| "journal_entry", | |
| file_token, | |
| "application/pdf" | |
| ) | |
| {:ok, invoice_document} = add_invoice_document(new_invoice, upload_document) | |
| {:ok, invoice} = insert_new_invoice(new_invoice, invoice_document) | |
| attach_invoice_documentation(invoice, invoice_document) | |
| set_project_last_synced_at_date(project) | |
| invoice | |
| end) | |
| document_to_update -> | |
| update_existing_yardi_journal_entry( | |
| document_to_update, | |
| journal_entry, | |
| project.organization, | |
| project | |
| ) | |
| end | |
| end | |
| defp update_existing_yardi_journal_entry(document_to_update, journal_entry, vendor, project) do | |
| if is_nil(document_to_update.draw) or document_to_update.draw.state != "funded" do | |
| query = | |
| from invoice in InvoiceSchema, | |
| where: invoice.document_id == ^document_to_update.id | |
| invoice_to_update = Repo.one(query) | |
| line_items = get_formatted_line_items(journal_entry.details, project) | |
| invoice_attrs = %UpdateInvoice{ | |
| description: get_description(journal_entry), | |
| draw_id: invoice_to_update.draw_id, | |
| vendor_id: if(is_nil(vendor), do: nil, else: vendor.id), | |
| net_amount: to_money(journal_entry.total_amount), | |
| gross_amount: to_money(journal_entry.total_amount), | |
| line_items: line_items, | |
| date: journal_entry.journal_entry_date, | |
| type: "invoice", | |
| state: | |
| if is_nil(vendor) or Enum.empty?(line_items) do | |
| :failed | |
| else | |
| :applied | |
| end, | |
| auto_calculate_retainage: invoice_to_update.auto_calculate_retainage, | |
| total_retainage: invoice_to_update.total_retainage, | |
| current_payment_due: invoice_to_update.current_payment_due | |
| } | |
| import_source_metadata = %{ | |
| yardi_id: journal_entry.id, | |
| yardi_document_type: "journal_entry", | |
| yardi_status: "updated", | |
| last_yardi_sync: DateTime.utc_now(), | |
| vendor_id: if(journal_entry.person_id == "", do: nil, else: journal_entry.person_id), | |
| invoice_date: parse_invoice_date(journal_entry.journal_entry_date), | |
| gross_amount: to_money(journal_entry.total_amount), | |
| line_items: line_items, | |
| is_reversed: journal_entry_is_reversed?(journal_entry.reference), | |
| amount_paid: nil, | |
| date_paid: nil, | |
| invoice_number: nil, | |
| is_paid: false | |
| } | |
| document_attrs = %{ | |
| vendor_id: invoice_attrs.vendor_id, | |
| import_source_metadata: import_source_metadata | |
| } | |
| document_to_update | |
| |> invoice_has_changed?(document_attrs) | |
| |> handle_update_invoice( | |
| document_to_update, | |
| document_attrs, | |
| invoice_to_update, | |
| invoice_attrs, | |
| project | |
| ) | |
| end | |
| end | |
| defp handle_update_invoice(false, _, _, invoice_to_update, _, _) do | |
| {:ok, invoice_to_update} | |
| end | |
| defp handle_update_invoice( | |
| true, | |
| document_to_update, | |
| document_attrs, | |
| invoice_to_update, | |
| invoice_attrs, | |
| project | |
| ) do | |
| Logger.warn("[WOOD] invoice has changed. invoice_to_update: #{inspect(invoice_to_update)}") | |
| Logger.warn("[WOOD] invoice has changed. invoice_attrs: #{inspect(invoice_attrs)}") | |
| Logger.warn("[WOOD] invoice has changed. document_to_update: #{inspect(document_to_update)}") | |
| Logger.warn("[WOOD] invoice has changed. document_attrs: #{inspect(document_attrs)}") | |
| Repo.transaction(fn -> | |
| {:ok, updated_document} = | |
| document_to_update | |
| |> Document.changeset(document_attrs) | |
| |> Repo.update() | |
| {:ok, updated_invoice} = | |
| invoice_to_update | |
| |> InvoiceSchema.changeset_from_update_invoice(invoice_attrs) | |
| |> Repo.update() | |
| attach_invoice_documentation(updated_invoice, updated_document) | |
| set_project_last_synced_at_date(project) | |
| updated_invoice | |
| end) | |
| end | |
| # for the payable case, the invoice_changeset and the document's amount_paid and is_paid | |
| # fields will have all the potential changes coming from yardi | |
| defp invoice_has_changed?( | |
| %Document{ | |
| import_source_metadata: original_import_source_metadata | |
| }, | |
| %{import_source_metadata: import_source_metadata} | |
| ) do | |
| formatted_original_details = | |
| Enum.map(original_import_source_metadata.line_items, fn detail -> | |
| Map.new(detail, fn {key, value} -> {String.to_atom(key), value} end) | |
| end) | |
| # remove variable fields | |
| cleaned_original_import_source_metadata = | |
| original_import_source_metadata | |
| |> Map.from_struct() | |
| |> Map.put(:line_items, formatted_original_details) | |
| |> Map.drop([:yardi_status, :last_yardi_sync, :id]) | |
| cleaned_import_source_metadata = | |
| Map.drop(import_source_metadata, [:yardi_status, :last_yardi_sync]) | |
| # the yardi data has NOT changed from its original if the metadata's are the same | |
| # NOTE: the Rabbet invoice may have changed but we don't check for that | |
| # We only want to update a Rabbet invoice if data coming from Yardi has been updated | |
| !Map.equal?(cleaned_import_source_metadata, cleaned_original_import_source_metadata) | |
| end | |
| defp get_existing_invoice(yardi_document, project_id) do | |
| query = | |
| from document in Document, | |
| where: fragment("import_source_metadata->>'yardi_id'=?", ^yardi_document.id), | |
| where: document.type == "invoice", | |
| where: document.project_id == ^project_id, | |
| order_by: [desc: document.inserted_at], | |
| limit: 1, | |
| preload: [:draw] | |
| Repo.one(query) | |
| end | |
| defp payable_to_new_invoice(payable, project, vendor) do | |
| attrs = %{ | |
| project: project.id, | |
| vendor: vendor, | |
| date: payable.invoice_date, | |
| number: payable.invoice_number | |
| } | |
| new_invoice(payable, attrs) | |
| end | |
| defp journal_entry_to_new_invoice(journal_entry, project, vendor) do | |
| attrs = %{ | |
| project: project.id, | |
| vendor: vendor, | |
| date: journal_entry.journal_entry_date, | |
| number: nil | |
| } | |
| new_invoice(journal_entry, attrs) | |
| end | |
| defp new_invoice(document, attrs) do | |
| %{ | |
| project: project, | |
| vendor: vendor, | |
| date: date, | |
| number: number | |
| } = attrs | |
| line_items = get_formatted_line_items(document.details, project) | |
| has_non_matching_line_items = | |
| Enum.any?(line_items, fn line_item -> | |
| Map.get(line_item, :line_item_id) == nil | |
| end) | |
| state = | |
| if is_nil(vendor) or Enum.empty?(line_items) or has_non_matching_line_items, | |
| do: :failed, | |
| else: :applied | |
| %NewInvoice{ | |
| id: UUID.generate(), | |
| project_id: project.id, | |
| draw_id: nil, | |
| type: "invoice", | |
| description: get_description(document), | |
| vendor_id: if(is_nil(vendor), do: nil, else: vendor.id), | |
| net_amount: to_money(document.total_amount), | |
| gross_amount: to_money(document.total_amount), | |
| line_items: line_items, | |
| state: state, | |
| date: date, | |
| number: number | |
| } | |
| end | |
| # if a payable has any details whose property_id | |
| # does not match the project_custom_id, | |
| # remove that detail from the payable | |
| # and remove that detail's amount from the payable's amount | |
| defp fix_payable_details(payable, project_custom_id) do | |
| {filtered_non_matching_details, filtered_details} = | |
| Enum.split_with(payable.details, fn detail -> | |
| is_voided = Map.get(detail, :is_voided, "false") | |
| detail.property_id != project_custom_id || is_voided == "true" | |
| end) | |
| if length(filtered_non_matching_details) != 0 do | |
| filtered_non_matching_details_total_amount = | |
| Enum.reduce(filtered_non_matching_details, 0, fn detail, total -> | |
| {detail_amount, _} = Float.parse(detail.amount) | |
| detail_amount + total | |
| end) | |
| filtered_matching_details_total_amount = | |
| Enum.reduce(filtered_details, 0, fn detail, total -> | |
| {detail_amount, _} = Float.parse(detail.amount) | |
| detail_amount + total | |
| end) | |
| corrected_payable_amount_paid = | |
| case Float.parse(payable.amount_paid) do | |
| {0.0, _} -> | |
| 0 | |
| {payable_amount_paid, _} -> | |
| payable_amount_paid - filtered_non_matching_details_total_amount | |
| end | |
| payable | |
| |> Map.put( | |
| :total_amount, | |
| filtered_matching_details_total_amount | |
| ) | |
| |> Map.put( | |
| :amount_paid, | |
| corrected_payable_amount_paid | |
| ) | |
| |> Map.put(:details, filtered_details) | |
| else | |
| details_amount_total = | |
| Enum.reduce(payable.details, 0, fn detail, total -> | |
| {detail_amount, _} = Float.parse(detail.amount) | |
| detail_amount + total | |
| end) | |
| {payable_total_amount, _} = Float.parse(payable.total_amount) | |
| if details_amount_total != payable_total_amount do | |
| payable | |
| |> Map.put(:total_amount, details_amount_total) | |
| else | |
| payable | |
| end | |
| end | |
| end | |
| defp filter_details(details, project_custom_id) do | |
| Enum.filter(details, fn detail -> | |
| detail.property_id == project_custom_id && detail.category_id !== "" | |
| end) | |
| end | |
| defp aggregate_journal_entry_amounts(details) do | |
| Enum.reduce(details, 0, fn detail, total -> | |
| {detail_amount, _} = Float.parse(detail.amount) | |
| detail_amount + total | |
| end) | |
| end | |
| defp split_journal_entry_by_details(journal_entry, details) do | |
| Enum.map(details, fn detail -> | |
| {detail_amount, _} = Float.parse(detail.amount) | |
| journal_entry_id = "#{journal_entry.id}-#{detail.category_id}-#{detail.amount}" | |
| journal_entry | |
| |> Map.put(:id, journal_entry_id) | |
| |> Map.put(:details, [detail]) | |
| |> Map.put(:total_amount, detail_amount) | |
| end) | |
| end | |
| defp get_formatted_line_items(details, project) do | |
| details | |
| |> Enum.with_index(fn detail, index -> | |
| budget_line_item = get_budget_line_item_from_detail(detail, project) | |
| if is_nil(budget_line_item) do | |
| %{ | |
| name: nil, | |
| number: nil, | |
| amount: detail.amount, | |
| gross_amount: detail.amount, | |
| line_item_index: index, | |
| line_item_id: nil, | |
| retainage_amount: 0, | |
| retainage_to_date_amount: 0, | |
| materials_stored_amount: 0 | |
| } | |
| else | |
| %{ | |
| name: budget_line_item.name, | |
| number: budget_line_item.number, | |
| amount: detail.amount, | |
| gross_amount: detail.amount, | |
| line_item_index: index, | |
| line_item_id: budget_line_item.id, | |
| retainage_amount: 0, | |
| retainage_to_date_amount: 0, | |
| materials_stored_amount: 0 | |
| } | |
| end | |
| end) | |
| |> Enum.filter(fn line_item -> | |
| !is_nil(line_item) | |
| end) | |
| end | |
| defp get_budget_line_item_from_detail(detail, project) do | |
| query = | |
| from budget_line_item in BudgetLineItem, | |
| where: budget_line_item.number == ^detail.category_id, | |
| where: budget_line_item.project_id == ^project.id | |
| query | |
| |> Repo.all() | |
| |> List.first() | |
| end | |
| defp get_description(document) do | |
| detail_notes = | |
| Enum.reduce(document.details, "Detail Notes: ", fn detail, text -> | |
| text <> "#{detail.category_id} (#{to_money(detail.amount)}) - #{detail.notes}\r" | |
| end) | |
| "Note: #{document.notes}\r" <> detail_notes | |
| end | |
| defp get_file_token_for_payable(invoice, project, organization, payable, url, auth) do | |
| case Api.get_invoice_image_from_yardi( | |
| url, | |
| auth, | |
| project.custom_id, | |
| payable.id, | |
| invoice.number | |
| ) do | |
| {:ok, %{page_count: page_count}} -> | |
| {parsed_page_count, _} = Integer.parse(page_count) | |
| invoice_images = | |
| Stream.unfold(1, fn page -> | |
| if page == parsed_page_count + 1 do | |
| nil | |
| else | |
| {:ok, %{image: png}} = | |
| Api.get_invoice_image_from_yardi( | |
| url, | |
| auth, | |
| project.custom_id, | |
| payable.id, | |
| invoice.number, | |
| Integer.to_string(page) | |
| ) | |
| # other option is to turn into separate job | |
| Process.sleep(@api_rate_limit) | |
| {png, page + 1} | |
| end | |
| end) | |
| |> Enum.to_list() | |
| png_file_paths = | |
| Enum.map(invoice_images, fn image -> | |
| with {:ok, temp_file} <- Briefly.create(), | |
| {:ok, data} <- Base.decode64(image), | |
| :ok <- File.write(temp_file, data) do | |
| {:ok, temp_file} | |
| temp_file | |
| end | |
| end) | |
| combine_list_of_pngs_to_single_pdf(png_file_paths, parsed_page_count) | |
| _ -> | |
| create_pdf(invoice, project, organization, "payable-template") | |
| end | |
| end | |
| defp combine_list_of_pngs_to_single_pdf(png_file_paths, page_count) do | |
| {:ok, pdf} = | |
| png_file_paths | |
| |> Enum.map(fn path -> | |
| {:ok, doc} = Librarian.get_local(path) | |
| doc | |
| end) | |
| |> Librarian.combine() | |
| {:ok, pdf} = Librarian.upload(pdf) | |
| {:ok, | |
| %{storage_path: pdf.storage_path, content_type: pdf.content_type, page_count: page_count}} | |
| end | |
| defp create_pdf(invoice, project, organization, template_name) do | |
| with {:ok, file} <- create_docx(organization, project, invoice, template_name), | |
| {:ok, file} <- Librarian.transform_format(file, "pdf") do | |
| Librarian.upload(file) | |
| else | |
| error -> {:error, error} | |
| end | |
| end | |
| # How is this cyclomatically complex..? | |
| # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity | |
| defp create_docx(organization, project, invoice, template_name) do | |
| params = %{ | |
| context: %{ | |
| project: %{ | |
| name: project.name | |
| }, | |
| organization: %{ | |
| street_address: organization.street_address || "", | |
| city: organization.city || "", | |
| state: organization.state || "", | |
| zip: organization.zip || "", | |
| logo_token: organization.alternate_logo_file_token | |
| }, | |
| invoice: %{ | |
| number: invoice.number || "", | |
| date: | |
| if(is_nil(invoice.date), | |
| do: "", | |
| else: format_date(invoice.date) | |
| ), | |
| amount: invoice.net_amount |> Money.to_decimal() |> Decimal.to_float() | |
| }, | |
| stakeholders: %{ | |
| client: client_params(invoice.vendor_id) | |
| }, | |
| line_items: line_item_params(invoice) | |
| }, | |
| template_name: template_name | |
| } | |
| Draftsman.generate_invoice(params) | |
| end | |
| defp line_item_params(invoice) when length(invoice.line_items) != 0 do | |
| Enum.map(invoice.line_items, fn line_item -> | |
| {line_item_amount, _} = Float.parse(line_item.amount) | |
| %{ | |
| number: line_item.number, | |
| name: line_item.name, | |
| amount: line_item_amount | |
| } | |
| end) | |
| end | |
| defp line_item_params(_invoice) do | |
| [ | |
| %{ | |
| number: "", | |
| name: "", | |
| amount: "" | |
| } | |
| ] | |
| end | |
| defp client_params(nil) do | |
| %{ | |
| id: "", | |
| name: "", | |
| address: "", | |
| city: "", | |
| state: "", | |
| zip: "", | |
| phone_number: "" | |
| } | |
| end | |
| defp client_params(vendor_id) do | |
| vendor = Repo.get!(Organization, vendor_id) | |
| %{ | |
| id: vendor.vendor_cost_code || "", | |
| name: vendor.name || "", | |
| address: vendor.street_address || "", | |
| city: vendor.city || "", | |
| state: vendor.state || "", | |
| zip: vendor.zip || "", | |
| phone_number: Enum.join(vendor.phone_numbers, " | ") | |
| } | |
| end | |
| defp add_upload_document( | |
| project, | |
| vendor, | |
| invoice, | |
| yardi_document, | |
| yardi_document_type, | |
| file_token, | |
| content_type, | |
| page_count \\ 1 | |
| ) do | |
| import_source_metadata = | |
| if yardi_document_type == "payable" do | |
| %{ | |
| yardi_id: yardi_document.id, | |
| yardi_document_type: "payable", | |
| yardi_status: "added", | |
| last_yardi_sync: DateTime.utc_now(), | |
| vendor_id: yardi_document.person_id, | |
| invoice_number: yardi_document.invoice_number, | |
| invoice_date: yardi_document.invoice_date, | |
| is_paid: !convert_to_boolean(yardi_document.is_unpaid), | |
| amount_paid: to_money(yardi_document.amount_paid), | |
| date_paid: get_date_paid_from_payable_details(yardi_document.details), | |
| gross_amount: to_money(yardi_document.total_amount), | |
| is_reversed: convert_to_boolean(yardi_document.is_reversed), | |
| line_items: invoice.line_items | |
| } | |
| else | |
| %{ | |
| yardi_id: yardi_document.id, | |
| yardi_document_type: "journal_entry", | |
| yardi_status: "added", | |
| last_yardi_sync: DateTime.utc_now(), | |
| vendor_id: yardi_document.person_id, | |
| invoice_number: nil, | |
| invoice_date: yardi_document.journal_entry_date, | |
| is_paid: false, | |
| amount_paid: nil, | |
| date_paid: nil, | |
| gross_amount: to_money(yardi_document.total_amount), | |
| is_reversed: journal_entry_is_reversed?(yardi_document.reference), | |
| line_items: invoice.line_items | |
| } | |
| end | |
| vendor_name = | |
| if is_nil(vendor) do | |
| "imported_from_yardi" | |
| else | |
| vendor.name | |
| end | |
| file_name = | |
| if content_type == "image/png" do | |
| "#{vendor_name} #{format_money(yardi_document.total_amount)}_#{yardi_document_type}.png" | |
| else | |
| "#{vendor_name} #{format_money(yardi_document.total_amount)}_#{yardi_document_type}.pdf" | |
| end | |
| attrs = %{ | |
| id: Ecto.UUID.generate(), | |
| file_content_type: content_type, | |
| original_file_content_type: content_type, | |
| file_token: file_token, | |
| original_file_token: file_token, | |
| file_name: file_name, | |
| original_file_name: file_name, | |
| state: Atom.to_string(invoice.state), | |
| pages: Enum.to_list(1..page_count), | |
| processing_started_at: DateTime.utc_now(), | |
| processing_finished_at: DateTime.utc_now(), | |
| organization_id: project.organization_id, | |
| project_id: project.id, | |
| uploader_id: nil, | |
| target_id: nil, | |
| vendor_id: invoice.vendor_id, | |
| import_source: "yardi", | |
| import_source_metadata: import_source_metadata | |
| } | |
| %Dozer.Documentation.Document{} | |
| |> Ecto.Changeset.change(attrs) | |
| |> Repo.insert() | |
| end | |
| defp get_date_paid_from_payable_details(payable_details) do | |
| if Enum.empty?(payable_details) do | |
| nil | |
| else | |
| payable_details | |
| |> hd() | |
| |> Map.get(:check_date) | |
| |> case do | |
| "" -> | |
| nil | |
| check_date -> | |
| {:ok, date_paid} = Timex.parse(check_date, "{ISO:Extended}") | |
| Timex.to_date(date_paid) | |
| end | |
| end | |
| end | |
| defp parse_invoice_date(invoice_date) do | |
| if invoice_date == "" do | |
| nil | |
| else | |
| {:ok, date} = Timex.parse(invoice_date, "{ISO:Extended}") | |
| Timex.to_date(date) | |
| end | |
| end | |
| def add_invoice_document(invoice, upload_document) do | |
| attrs = | |
| upload_document | |
| |> inherit_upload_attrs() | |
| |> Map.merge(%{ | |
| id: Ecto.UUID.generate(), | |
| upload_id: upload_document.id, | |
| correlation_id: upload_document.id, | |
| type: "invoice", | |
| state: if(invoice.state == :failed, do: "failed", else: "applied"), | |
| is_paid: upload_document.import_source_metadata.is_paid, | |
| amount_paid: | |
| if upload_document.import_source_metadata.is_paid == false do | |
| nil | |
| else | |
| upload_document.import_source_metadata.amount_paid | |
| end, | |
| date_paid: | |
| if upload_document.import_source_metadata.is_paid == false do | |
| nil | |
| else | |
| upload_document.import_source_metadata.date_paid | |
| end, | |
| import_source: upload_document.import_source, | |
| import_source_metadata: upload_document.import_source_metadata | |
| }) | |
| %Dozer.Documentation.Document{} | |
| |> Ecto.Changeset.change(attrs) | |
| |> Repo.insert() | |
| end | |
| defp insert_new_invoice(invoice, invoice_document) do | |
| attrs = %{ | |
| invoice | |
| | document_id: invoice_document.id | |
| } | |
| %InvoiceSchema{} | |
| |> InvoiceSchema.changeset_from_new_invoice(attrs) | |
| |> Repo.insert() | |
| end | |
| defp inherit_upload_attrs(upload) do | |
| Map.take(upload, [ | |
| :file_content_type, | |
| :original_file_content_type, | |
| :file_token, | |
| :original_file_token, | |
| :file_name, | |
| :original_file_name, | |
| :pages, | |
| :organization_id, | |
| :project_id, | |
| :uploader_id, | |
| :target_id, | |
| :vendor_id, | |
| :processing_started_at, | |
| :processing_finished_at | |
| ]) | |
| end | |
| defp attach_invoice_documentation(invoice, invoice_document) do | |
| {:ok, invoice} = Invoices.invoice_from_invoice_schema(invoice) | |
| Logger.warn( | |
| "[WOOD] In Yardi.attach_invoice_documentation: #{inspect(invoice)} #{inspect(invoice_document)}" | |
| ) | |
| VendorLineItems.sync_documentation(invoice) | |
| end | |
| defp set_project_last_synced_at_date(project) do | |
| project | |
| |> Project.changeset(%{accounts_payable_last_synced_at: Timex.now()}) | |
| |> Repo.update() | |
| end | |
| @doc """ | |
| Import and upsert vendors. | |
| TODO(mmiller): Finish with v2. | |
| """ | |
| def sync_vendors(organization_id) do | |
| organization = Repo.get!(Organization, organization_id) | |
| auth = auth_params(organization) | |
| url = organization.yardi_url <> @vendor_invoicing_endpoint | |
| with {:ok, vendors} <- Api.get_vendors(url, auth) do | |
| Enum.map(vendors, fn vendor -> | |
| attrs = %{ | |
| vendor_cost_code: vendor.vendor_id, | |
| name: vendor_name(vendor), | |
| city: vendor.city, | |
| state: sanitize_address_state(vendor.state), | |
| phone_numbers: vendor_phone_numbers(vendor), | |
| email_addresses: [vendor.email], | |
| street_address: vendor.address_1, | |
| type: "other", | |
| vendor_source: "yardi" | |
| } | |
| %Vendor{} | |
| |> Vendor.changeset(attrs) | |
| |> Ecto.Changeset.put_assoc(:customers, [organization]) | |
| end) | |
| end | |
| end | |
| @spec fetch_and_create_vendor_by_yardi_id(organization :: Organization.t(), id: String.t()) :: | |
| Vendor.t() | nil | |
| def fetch_and_create_vendor_by_yardi_id(organization, yardi_id) do | |
| auth_params = auth_params(organization) | |
| url = organization.yardi_url <> @vendor_invoicing_endpoint | |
| case Api.get_vendor_by_id(url, auth_params, yardi_id) do | |
| {:ok, vendor_from_api} -> | |
| {:ok, upserted_vendor} = generate_vendor_from_yardi(organization.id, vendor_from_api) | |
| upserted_vendor | |
| _ -> | |
| nil | |
| end | |
| end | |
| # Vendor data is not well formatted on their end. | |
| # Placing these in preferred order. | |
| defp vendor_name(vendor) do | |
| [ | |
| vendor.government_name, | |
| String.trim("#{vendor.vendor_first_name} #{vendor.vendor_last_name}"), | |
| vendor.vendor_id | |
| ] | |
| |> Enum.find(nil, fn name -> | |
| not is_nil(name) and name != "" | |
| end) | |
| end | |
| # Limitation of the xpath lib forces this. | |
| defp vendor_phone_numbers(vendor) do | |
| [ | |
| vendor.phone_number_1, | |
| vendor.phone_number_2, | |
| vendor.phone_number_3, | |
| vendor.phone_number_4, | |
| vendor.phone_number_5, | |
| vendor.phone_number_6, | |
| vendor.phone_number_7, | |
| vendor.phone_number_8, | |
| vendor.phone_number_9, | |
| vendor.phone_number_10 | |
| ] | |
| |> Enum.reject(&is_nil/1) | |
| |> Enum.map(&String.replace(&1, ~r/\D/, "")) | |
| |> Enum.reject(&is_nil/1) | |
| end | |
| defp auth_params(organization) do | |
| %AuthParams{ | |
| username: organization.yardi_user_name, | |
| password: organization.yardi_password, | |
| server_name: organization.yardi_server_name, | |
| database: organization.yardi_database, | |
| platform: platform(organization.yardi_platform), | |
| interface_entity: "Rabbet", | |
| interface_license: Application.get_env(:dozer, Dozer.Integrations.Yardi)[:license] | |
| } | |
| end | |
| def map_payable(project, invoice, organization) do | |
| %Payable{ | |
| person_id: invoice.vendor.vendor_cost_code, | |
| invoice_number: invoice.number, | |
| invoice_date: "#{invoice.date}T00:00:00", | |
| total_amount: money_fmt(invoice.net_amount), | |
| notes: payable_notes(invoice), | |
| details: Enum.map(invoice.line_items, &map_payable_detail(project, &1, organization)), | |
| custom: payable_custom(invoice) | |
| } | |
| end | |
| defp map_payable_detail(project, invoice_line_item, organization) do | |
| get_final_payable_detail( | |
| %PayableDetail{ | |
| amount: money_fmt(invoice_line_item.amount), | |
| account_id: gl_account(invoice_line_item, project), | |
| property_id: project.custom_id, | |
| notes: payable_detail_notes(invoice_line_item) | |
| }, | |
| invoice_line_item, | |
| Permissions.is_permitted?(organization, :yardi_job_cost_module) | |
| ) | |
| end | |
| defp get_final_payable_detail(preliminary_payable_detail, invoice_line_item, true) do | |
| job_cost_module_fields = %{ | |
| job_id: line_item_number(invoice_line_item), | |
| category_id: cost_code(invoice_line_item) | |
| } | |
| Map.merge(preliminary_payable_detail, job_cost_module_fields) | |
| end | |
| defp get_final_payable_detail(preliminary_payable_detail, _invoice_line_item, false) do | |
| preliminary_payable_detail | |
| end | |
| ## If there are no job cost codes, use the project's gl_code | |
| defp gl_account(%InvoiceLineItemSchema{job_cost_codes: []}, %Project{gl_code: account}), | |
| do: account | |
| ## If the first/only job cost code has a nil gl_code, use the project's gl_code | |
| defp gl_account( | |
| %InvoiceLineItemSchema{ | |
| job_cost_codes: [ | |
| %InvoiceLineItemJobCostCodeSchema{job_cost_code: %JobCostCode{gl_code: nil}} | |
| ] | |
| }, | |
| %Project{gl_code: account} | |
| ), | |
| do: account | |
| ## Otherwise, use the first/only job cost code's gl_code | |
| defp gl_account( | |
| %InvoiceLineItemSchema{ | |
| job_cost_codes: [ | |
| %InvoiceLineItemJobCostCodeSchema{job_cost_code: %JobCostCode{gl_code: account}} | |
| ] | |
| }, | |
| _project | |
| ), | |
| do: account | |
| ## If there is no Cost Code, return nil | |
| defp cost_code(%InvoiceLineItemSchema{job_cost_codes: []}), do: nil | |
| ## If there is a Cost Code, return it | |
| defp cost_code(%InvoiceLineItemSchema{ | |
| job_cost_codes: [ | |
| %InvoiceLineItemJobCostCodeSchema{job_cost_code: %JobCostCode{code: code}} | |
| ] | |
| }), | |
| do: code | |
| defp payable_notes(%{document: document}) do | |
| approval = DocumentReviews.get_approval_for_integration(document) | |
| case approval do | |
| nil -> nil | |
| _approval -> "Approved in Rabbet by #{approval.name}" | |
| end | |
| end | |
| # Returns either the line item number if it exists, or nil | |
| defp line_item_number(line) do | |
| case line.line_item do | |
| %{number: nil} -> nil | |
| # if you delete a line item number, instead of becoming nil, | |
| # it shows up as an empty string, so accounting for that case | |
| %{number: ""} -> nil | |
| _ -> line.line_item.number | |
| end | |
| end | |
| # Returns what we should put in the Notes field for the line item | |
| defp payable_detail_notes(line) do | |
| line_item_description = | |
| if line_item_number(line) == nil do | |
| line.name | |
| else | |
| "#{line_item_number(line)} - #{line.name}" | |
| end | |
| line_item_cost_code = | |
| if cost_code(line) == nil do | |
| "N/A" | |
| else | |
| cost_code(line) | |
| end | |
| """ | |
| Description: #{line_item_description}, | |
| Cost Code: #{line_item_cost_code} | |
| """ | |
| end | |
| defp journal_entry_is_reversed?(journal_entry_reference) do | |
| if String.contains?(journal_entry_reference, ["Reversal of", "reversal of"]) do | |
| true | |
| else | |
| false | |
| end | |
| end | |
| # Adds the URL to grab the PDF from | |
| defp payable_custom(invoice) do | |
| # Yardi forces this to https anyways so we're not | |
| # going to bother changing it for local dev. | |
| base = "https://#{DozerWeb.Endpoint.config(:url)[:host]}" | |
| url = "#{base}/download_document/#{invoice.document_id}/#{invoice.document.file_token}" | |
| %Custom{ | |
| interface_vendor: "Rabbet", | |
| custom_1: url | |
| } | |
| end | |
| defp get_project(project_id) do | |
| project_id | |
| |> Origination.get_project!() | |
| |> Repo.preload(:organization) | |
| end | |
| defp get_invoices(document_ids) do | |
| query = | |
| from i in InvoiceSchema, | |
| where: i.document_id in ^document_ids, | |
| preload: [ | |
| :vendor, | |
| :document, | |
| line_items: [:line_item, job_cost_codes: [:job_cost_code]] | |
| ] | |
| Repo.all(query) | |
| end | |
| defp get_from_date_and_to_date( | |
| nil = _accounts_payable_last_synced_at, | |
| accounts_payable_settings | |
| ) do | |
| now = Timex.now(@server_tz) | |
| day_range_for_pulling_payables = | |
| Map.get(accounts_payable_settings, :day_range_for_pulling_payables, 365) | |
| {Timex.shift(now, days: -day_range_for_pulling_payables), now} | |
| end | |
| defp get_from_date_and_to_date(accounts_payable_last_synced_at, _accounts_payable_settings) do | |
| from = Timex.Timezone.convert(accounts_payable_last_synced_at, @server_tz) | |
| to = Timex.now(@server_tz) | |
| {from, to} | |
| end | |
| defp platform("sql_server"), do: "SQL Server" | |
| defp platform("oracle"), do: "Oracle" | |
| defp platform(_), do: "SQL Server" | |
| defp money_fmt(amount) do | |
| amount | |
| |> Money.to_decimal() | |
| |> Decimal.to_float() | |
| |> :erlang.float_to_binary(decimals: 2) | |
| end | |
| defp convert_to_boolean("true"), do: true | |
| defp convert_to_boolean("false"), do: false | |
| def config(key) do | |
| Application.fetch_env!(:dozer, __MODULE__)[key] | |
| end | |
| defp set_organization_last_synced_at_date(organization) do | |
| organization | |
| |> Organization.update_changeset(%{yardi_vendors_last_synced_at: Timex.now()}) | |
| |> Repo.update() | |
| end | |
| defp get_vendor_phone_numbers(vendor) do | |
| phone_numbers = [ | |
| vendor.phone_number_1, | |
| vendor.phone_number_2, | |
| vendor.phone_number_3, | |
| vendor.phone_number_4, | |
| vendor.phone_number_5, | |
| vendor.phone_number_6, | |
| vendor.phone_number_7, | |
| vendor.phone_number_8, | |
| vendor.phone_number_9, | |
| vendor.phone_number_10 | |
| ] | |
| Enum.filter(phone_numbers, fn phone_number -> | |
| phone_number != "" | |
| end) | |
| end | |
| defp get_vendor_name(vendor) do | |
| cond do | |
| vendor.vendor_last_name != "" -> | |
| vendor.vendor_last_name | |
| vendor.government_name != "" -> | |
| vendor.government_name | |
| vendor.vendor_first_name != "" -> | |
| vendor.vendor_fist_name | |
| true -> | |
| "Yardi Generated Vendor" | |
| end | |
| end | |
| defp enqueue_payables(project_id, payables) do | |
| workers = | |
| Enum.map(payables, fn payable -> | |
| YardiPayableWorker.new(%{ | |
| project_id: project_id, | |
| payable: payable | |
| }) | |
| end) | |
| Oban.insert_all(workers) | |
| end | |
| defp enqueue_journal_entries(project_id, journal_entries) do | |
| workers = | |
| Enum.map(journal_entries, fn journal_entry -> | |
| YardiJournalEntryWorker.new(%{ | |
| project_id: project_id, | |
| journal_entry: journal_entry | |
| }) | |
| end) | |
| Oban.insert_all(workers) | |
| end | |
| defp update_existing_payable(document_to_update, payable, vendor, project) do | |
| query = from invoice in InvoiceSchema, where: invoice.document_id == ^document_to_update.id | |
| invoice_to_update = Repo.one(query) | |
| line_items = get_formatted_line_items(payable.details, project) | |
| invoice_attrs = %UpdateInvoice{ | |
| description: get_description(payable), | |
| draw_id: invoice_to_update.draw_id, | |
| vendor_id: if(is_nil(vendor), do: nil, else: vendor.id), | |
| net_amount: to_money(payable.total_amount), | |
| gross_amount: to_money(payable.total_amount), | |
| line_items: line_items, | |
| date: parse_invoice_date(payable.invoice_date), | |
| number: payable.invoice_number, | |
| type: "invoice", | |
| state: | |
| if is_nil(vendor) or Enum.empty?(line_items) do | |
| :failed | |
| else | |
| :applied | |
| end, | |
| auto_calculate_retainage: invoice_to_update.auto_calculate_retainage, | |
| total_retainage: invoice_to_update.total_retainage, | |
| current_payment_due: invoice_to_update.current_payment_due | |
| } | |
| import_source_metadata = %{ | |
| yardi_id: payable.id, | |
| yardi_document_type: "payable", | |
| yardi_status: "updated", | |
| last_yardi_sync: DateTime.utc_now(), | |
| vendor_id: payable.person_id, | |
| invoice_number: payable.invoice_number, | |
| invoice_date: parse_invoice_date(payable.invoice_date), | |
| is_paid: !convert_to_boolean(payable.is_unpaid), | |
| amount_paid: to_money(payable.amount_paid), | |
| date_paid: get_date_paid_from_payable_details(payable.details), | |
| gross_amount: to_money(payable.total_amount), | |
| is_reversed: convert_to_boolean(payable.is_reversed), | |
| line_items: line_items | |
| } | |
| document_attrs = %{ | |
| is_paid: import_source_metadata.is_paid, | |
| amount_paid: | |
| if import_source_metadata.is_paid == false do | |
| nil | |
| else | |
| import_source_metadata.amount_paid | |
| end, | |
| date_paid: | |
| if import_source_metadata.is_paid == false do | |
| nil | |
| else | |
| import_source_metadata.date_paid | |
| end, | |
| vendor_id: invoice_attrs.vendor_id, | |
| import_source_metadata: import_source_metadata | |
| } | |
| document_to_update | |
| |> invoice_has_changed?(document_attrs) | |
| |> handle_update_invoice( | |
| document_to_update, | |
| document_attrs, | |
| invoice_to_update, | |
| invoice_attrs, | |
| project | |
| ) | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment