Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task to export calculated numbers of the monthly reports back to HubSpot #6439

Merged
merged 1 commit into from
Jul 23, 2024

Conversation

marcospri
Copy link
Member

@marcospri marcospri commented Jul 10, 2024

For:

This PR needs a follow up to schedule the task in h-periodic.

To summarize the process we:

  • Fetch HubSpot companies with tasks.hubspot.refresh_hubspot_data
  • We generate monthly usage reports for thos companies with task.organization.schedule_monthly_deal_report

now, with this commit we:

  • Push the calculated numbers (unique user and teachers) to the corresponding fields in HubSpot companies.

Testing

Describing here the process I followed locally to test this end to end.

  • Fetch all HubSpot companies:
refresh_hubspot_data.delay()
  • Modify some of these companies to match your local organizations, eg:

update hubspot_company set lms_organization_id = 'local.lms.org.FFIHD9f1RwyLPfx14zM_7Q' where id = 11;

  • Generate/Insert some reports that match that organization:
select * from organization_usage_report where organization_id in (1,2);
 id | organization_id |     tag      |                                   key                                   |   since    |   until    | unique_users | report |          created           |          updated           | unique_teachers 
----+-----------------+--------------+-------------------------------------------------------------------------+------------+------------+--------------+--------+----------------------------+----------------------------+-----------------
 11 |               1 | monthly-deal | local.lms.org.LDtcl7EUTeW2UERPJLAVtA-monthly-deal-2023-01-01-2023-07-31 | 2023-01-01 | 2023-07-31 |            0 | []     | 2024-06-18 10:33:22.193294 | 2024-06-18 10:33:22.193294 |               0
  2 |               2 | monthly-deal | local.lms.org.LDtcl7EUTeW2UERPJLAVtA-monthly-deal-2023-01-01-2024-01-31 | 2023-01-01 | 2024-01-31 |            0 | []     | 2024-06-18 10:29:28.260707 | 2024-06-18 10:29:28.260707 |               0
  • Finally, run the new task:
from lms.tasks.hubspot import export_companies_contract_billables; 
export_companies_contract_billables.delay()
  • Let's check that the import actually happened, we log into HuBSpot, and go to the imports in: https://app.hubspot.com/import/21081992
    Screenshot from 2024-07-10 15-07-12

  • And we can see the records we modified locally to match the HubSpot data being updated and the rest of the rows having no value (--) in those columns.

Screenshot from 2024-07-10 15-09-17

@@ -1,5 +1,22 @@
import csv
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of this code is copied and adapted from the reporting system.

I've avoided many of the abstractions there, so here we hard code the columns we are going to send for companies for example.

I don't think we are going to be modifying this very often right now but we can move into abstractions as we apply this to more models and/or columns.

}
]

import_request = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We name here the import job on HubSpot side.

@@ -27,27 +27,26 @@ def get_company(self, organization_id: str) -> HubSpotCompany | None:
# More than one company pointing to the same org is a data entry error, ignore them.
return None

def get_companies_with_active_deals(self, date_: date):
def _companies_with_active_deals_query(self, date_: date):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just making the existing query a private method to be able to reusing it for the new code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

def export_companies_contract_billables(self, date_: date):
"""Export the contract billable numbers to HubSpot."""
query = (
self._companies_with_active_deals_query(date_)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the existing companies_with_active_deals_query(date_) query

"""Export the contract billable numbers to HubSpot."""
query = (
self._companies_with_active_deals_query(date_)
.join(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Join with organizations and reports.

Organization.public_id == HubSpotCompany.lms_organization_id,
)
.join(OrganizationUsageReport)
.distinct(OrganizationUsageReport.organization_id)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Picking just one report per organization.

.distinct(OrganizationUsageReport.organization_id)
.order_by(
OrganizationUsageReport.organization_id,
OrganizationUsageReport.until.desc(),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Picking the last report.

OrganizationUsageReport.organization_id,
OrganizationUsageReport.until.desc(),
)
).with_only_columns(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And only picking the values we need to pass to the client to generate the CSV.

csv.writer.return_value.writerow.assert_called_once_with(
(sentinel.id, sentinel.teachers, sentinel.users)
)
api_client.crm.imports.core_api.create.assert_called_once_with(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this level we just deal with the HubSpot API, we assert we are calling it with the expected values.

def test_export_companies_contract_billables(
self, svc, hubspot_api_client, db_session
):
organization = factories.Organization(public_id="TEST_ORG")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this level we care about picking the right data, we set up the data in the DB and assert we get the right values to call the client.

@marcospri marcospri requested a review from seanh July 10, 2024 15:18
Copy link
Collaborator

@seanh seanh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion for some more concise and consistent naming: export_billables() (Celery task) --calls-> HubSpotService.export_billables(date) --calls-> HubSpotClient.import_billables(billables)

lms/services/hubspot/_client.py Outdated Show resolved Hide resolved
lms/services/hubspot/_client.py Outdated Show resolved Hide resolved
lms/services/hubspot/_client.py Outdated Show resolved Hide resolved
]

import_request = {
"name": f"contract_year_import_{date.today().isoformat()}",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the dates should probably be part of the contract_year_billables argument, rather than using the current day here. Those numbers of unique teachers and users in contract_year_billables were calculated at to a given date, it'd be better to include that date in the data rather than implicitly assuming that it matches date.today() at the time when this line later runs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have included this as a second independent parameter

@@ -27,27 +27,26 @@ def get_company(self, organization_id: str) -> HubSpotCompany | None:
# More than one company pointing to the same org is a data entry error, ignore them.
return None

def get_companies_with_active_deals(self, date_: date):
def _companies_with_active_deals_query(self, date_: date):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


# From the point of view of HubSpot we are creating an import
self._client.create_company_import(
self._db.execute(query).all(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where you should probably include date_ in the arguments to create_company_import()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done that

To summarize the process we:

- Fetch HubSpot companies with tasks.hubspot.refresh_hubspot_data
- We generate monthly usage reports for thos companies with
  task.organization.schedule_monthly_deal_report

now, with this commit we:

- Push the calculated numbers (unique user and teachers) to the
  corresponding fields in HubSpot companies.
@marcospri
Copy link
Member Author

Merged the naming suggestions 👍

Run the task locally to make sure everything lines up. A new import was created here https://app.hubspot.com/import/21081992

@marcospri marcospri requested a review from seanh July 12, 2024 08:43
@marcospri marcospri merged commit 4c6a2a2 into main Jul 23, 2024
9 checks passed
@marcospri marcospri deleted the hubpost-export branch July 23, 2024 07:25
Copy link

sentry-io bot commented Jul 23, 2024

Suspect Issues

This pull request was deployed and Sentry observed the following issues:

  • ‼️ ApiException: (400) lms.tasks.hubspot.export_companies_contract_bil... View Issue

Did you find this useful? React with a 👍 or 👎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants