diff --git a/microsetta_private_api/admin/admin_impl.py b/microsetta_private_api/admin/admin_impl.py index b11236bc5..0766cd8b9 100644 --- a/microsetta_private_api/admin/admin_impl.py +++ b/microsetta_private_api/admin/admin_impl.py @@ -35,6 +35,7 @@ from werkzeug.exceptions import Unauthorized from microsetta_private_api.qiita import qclient from microsetta_private_api.repo.interested_user_repo import InterestedUserRepo +from microsetta_private_api.model.subscription import FULFILLMENT_ACCOUNT_ID def search_barcode(token_info, sample_barcode): @@ -551,7 +552,7 @@ def create_daklapack_order_internal(order_dict): with Transaction() as t: account_repo = AccountRepo(t) order_dict[SUBMITTER_ACCT_KEY] = account_repo.get_account( - "000fc4cd-8fa4-db8b-e050-8a800c5d81b7") + FULFILLMENT_ACCOUNT_ID) result = _create_daklapack_order(order_dict) return result diff --git a/microsetta_private_api/admin/email_templates.py b/microsetta_private_api/admin/email_templates.py index d82dfcc71..a6846b82e 100644 --- a/microsetta_private_api/admin/email_templates.py +++ b/microsetta_private_api/admin/email_templates.py @@ -97,7 +97,7 @@ class EmailMessage(Enum): ) kit_tracking_number = ( gettext( - "Your Kit is on its way!"), + "Your kit is on its way!"), "email/kit_tracking_number.jinja2", ("first_name", "tracking_number"), EventType.EMAIL, diff --git a/microsetta_private_api/model/subscription.py b/microsetta_private_api/model/subscription.py index 70494f9a8..0b8888cd6 100644 --- a/microsetta_private_api/model/subscription.py +++ b/microsetta_private_api/model/subscription.py @@ -1,5 +1,7 @@ from microsetta_private_api.model.model_base import ModelBase +FULFILLMENT_ACCOUNT_ID = "000fc4cd-8fa4-db8b-e050-8a800c5d81b7" + class Subscription(ModelBase): def __init__(self, **kwargs): diff --git a/microsetta_private_api/repo/perk_fulfillment_repo.py b/microsetta_private_api/repo/perk_fulfillment_repo.py index 3f52ba6e1..f546e20b1 100644 --- a/microsetta_private_api/repo/perk_fulfillment_repo.py +++ b/microsetta_private_api/repo/perk_fulfillment_repo.py @@ -72,15 +72,39 @@ def process_pending_fulfillments(self): rows = cur.fetchall() for row in rows: - if (row['ffq_quantity'] > 1 or row['kit_quantity'] > 1) and \ - row['fulfillment_spacing_number'] > 0: + if self._is_subscription(row): subscription_id = \ self._create_subscription(row['payer_email'], row['transaction_id'], row['ftp_id']) + else: + subscription_id = None + + # If there are any FFQs attached to the perk, immediately + # fulfill the first one + if row['ffq_quantity'] > 0: + # If the perk is a kit or subscription, send thank you + # email with kit content. Otherwise, send thank you + # for FFQ only + if row['kit_quantity'] > 0: + template = "thank_you_with_kit" + else: + template = "thank_you_no_kit" + + error_info = self._fulfill_ffq( + row['ftp_id'], + template, + row['payer_email'], + row['first_name'], + subscription_id + ) + if error_info is not None: + error_report.append(error_info) - for x in range(row['ffq_quantity']): - if x > 0 and row['fulfillment_spacing_number'] > 0: + # Then, if there are more FFQs, schedule/fulfill them as + # appropriate based on fulfillment_spacing_number + for x in range(1, row['ffq_quantity']): + if row['fulfillment_spacing_number'] > 0: fulfillment_date =\ self._future_fulfillment_date( row['fulfillment_spacing_number'], @@ -90,45 +114,29 @@ def process_pending_fulfillments(self): self._schedule_ffq(subscription_id, fulfillment_date, False) else: - registration_code = self._fulfill_ffq( - row['ftp_id'] + error_info = self._fulfill_ffq( + row['ftp_id'], + row['kit_quantity'], + row['payer_email'], + row['first_name'] ) + if error_info is not None: + error_report.append(error_info) + + # If there are any kits attached to the perk, immediately + # fulfill the first one + if row['kit_quantity'] > 0: + status, return_val = self._fulfill_kit( + row, + 1, + subscription_id + ) + if not status: + # Daklapack order failed, let the error percolate + error_report.append(return_val) - # If the perk is a kit or subscription, send thank you - # email with kit content. Otherwise, send thank you - # for FFQ only - if row['kit_quantity'] > 0: - template = "thank_you_with_kit" - else: - template = "thank_you_no_kit" - - try: - send_email( - row['payer_email'], - template, - { - "first_name": row['first_name'], - "registration_code": registration_code, - "interface_endpoint": - SERVER_CONFIG["interface_endpoint"] - }, - EN_US - ) - except: # noqa - # try our best to email - pass - - if row['ffq_quantity'] > 0 and\ - row['fulfillment_spacing_number'] > 0: - # If this is the first FFQ of a subscription, - # we mark it as both scheduled and fulfilled - cur_date = datetime.now() - cur_date = cur_date.strftime("%Y-%m-%d") - self._schedule_ffq(subscription_id, cur_date, - True) - - for x in range(row['kit_quantity']): - if x > 0 and row['fulfillment_spacing_number'] > 0: + for x in range(1, row['kit_quantity']): + if row['fulfillment_spacing_number'] > 0: fulfillment_date =\ self._future_fulfillment_date( row['fulfillment_spacing_number'], @@ -140,47 +148,14 @@ def process_pending_fulfillments(self): row['dak_article_code'], False) else: - country = pycountry.countries.get( - alpha_2=row['country'] - ) - country_name = country.name - - projects =\ - self._campaign_id_to_projects(row['campaign_id']) - address_dict = { - "firstName": row['first_name'], - "lastName": row['last_name'], - "address1": row['address_1'], - "insertion": "", - "address2": row['address_2'], - "postalCode": row['postal_code'], - "city": row['city'], - "state": row['state'], - "country": country_name, - "countryCode": row['country'], - "phone": row['phone'] - } status, return_val = self._fulfill_kit( - row['ftp_id'], - projects, - row['dak_article_code'], - 1, - address_dict + row, + 1 ) if not status: # Daklapack order failed, let the error percolate error_report.append(return_val) - if row['kit_quantity'] > 0 and\ - row['fulfillment_spacing_number'] > 0: - # If this is the first kit of a subscription, - # we mark it as both scheduled and fulfilled - cur_date = datetime.now() - cur_date = cur_date.strftime("%Y-%m-%d") - self._schedule_kit(subscription_id, - cur_date, - row['dak_article_code'], - True) cur.execute( "UPDATE campaign.fundrazr_transaction_perk " "SET processed = true " @@ -197,11 +172,9 @@ def process_subscription_fulfillments(self): cur.execute( "SELECT sf.fulfillment_id, sf.fulfillment_type, " "sf.dak_article_code, ftp.id ftp_id, ft.payer_email, " - "iu.first_name iu_first_name, iu.last_name iu_last_name, " - "iu.phone iu_phone, iu.address_1 iu_address_1, " - "iu.address_2 iu_address_2, iu.city iu_city, " - "iu.state iu_state, iu.postal_code iu_postal_code, " - "iu.country iu_country, iu.campaign_id, s.account_id, " + "iu.first_name, iu.last_name, iu.phone, iu.address_1, " + "iu.address_2, iu.city, iu.state, iu.postal_code, " + "iu.country, iu.campaign_id, s.account_id, " "a.email a_email, a.first_name a_first_name, " "a.last_name a_last_name, a.street a_address_1, " "a.city a_city, a.state a_state, a.post_code a_postal_code, " @@ -217,105 +190,55 @@ def process_subscription_fulfillments(self): "ON ft.interested_user_id = iu.interested_user_id " "LEFT JOIN ag.account a" "ON s.account_id = a.id" - "WHERE sf.fulfilled = false AND sf.cancelled = FALSE " + "WHERE sf.fulfilled = FALSE AND sf.cancelled = FALSE " "AND sf.fulfillment_date <= CURRENT_DATE" ) rows = cur.fetchall() for row in rows: - if row['fulfillment_type'] == "ffq": - registration_code = self._fulfill_ffq(row['ftp_id']) + fulfillment_error = False + if row['fulfillment_type'] == "ffq": # If an account is linked to the subscription, we use # that account's first name and email - if row['account_id']: + if row['account_id'] is not None: email = row['a_email'] - first_name = row['first_name'] + first_name = row['a_first_name'] # If no account, fall back to original Fundrazr data else: email = row['payer_email'] - first_name = row['iu_first_name'] - - try: - send_email( - email, - "subscription_ffq_code", - { - "first_name": first_name, - "registration_code": registration_code, - "interface_endpoint": - SERVER_CONFIG["interface_endpoint"] - }, - EN_US - ) - except: # noqa - # try our best to email - pass - - elif row['fulfillment_type'] == "kit": - projects = \ - self._campaign_id_to_projects(row['campaign_id']) + first_name = row['first_name'] - if row['account_id']: - country = pycountry.countries.get( - alpha_2=row['a_country'] - ) - country_name = country.name - - address_dict = { - "firstName": row['a_first_name'], - "lastName": row['a_last_name'], - "address1": row['a_address_1'], - "insertion": "", - "address2": "", - "postalCode": row['a_postal_code'], - "city": row['a_city'], - "state": row['a_state'], - "country": country_name, - "countryCode": row['a_country'], - "phone": row['a_phone'] - } - else: - country = pycountry.countries.get( - alpha_2=row['country'] - ) - country_name = country.name - - address_dict = { - "firstName": row['iu_first_name'], - "lastName": row['iu_last_name'], - "address1": row['iu_address_1'], - "insertion": "", - "address2": row['iu_address_2'], - "postalCode": row['iu_postal_code'], - "city": row['iu_city'], - "state": row['iu_state'], - "country": country_name, - "countryCode": row['iu_country'], - "phone": row['iu_phone'] - } + email_error = self._fulfill_ffq( + row['ftp_id'], + "subscription_ffq_code", + email, + first_name + ) + if email_error is not None: + fulfillment_error = True + error_report.append(email_error) + elif row['fulfillment_type'] == "kit": status, return_val = \ - self._fulfill_kit(row['ftp_id'], - projects, - row['dak_article_code'], - 1, - address_dict) + self._fulfill_kit(row, 1) if not status: # Daklapack order failed, let the error percolate error_report.append(return_val) else: + fulfillment_error = True error_report.append( f"Subscription fulfillment {row['fulfillment_id']} " f"contains malformed fulfillment_type " - f"{row['fulfillmnet_type']}" + f"{row['fulfillment_type']}" ) - cur.execute( - "UPDATE campaign.subscriptions_fulfillment " - "SET fulfilled = true " - "WHERE fulfillment_id = %s", - (row['fulfillment_id'], ) - ) + if not fulfillment_error: + cur.execute( + "UPDATE campaign.subscriptions_fulfillment " + "SET fulfilled = true " + "WHERE fulfillment_id = %s", + (row['fulfillment_id'], ) + ) def check_for_shipping_updates(self): """Find orders for which we have not provided a tracking number, @@ -352,25 +275,65 @@ def check_for_shipping_updates(self): }, EN_US ) + cur.execute( + "UPDATE campaign.fundrazr_daklapack_orders " + "SET tracking_sent = true " + "WHERE fundrazr_transaction_perk_id = %s " + "AND dak_order_id = %s", + (r['fundrazr_transaction_perk_id'], r['dak_order_id']) + ) except: # noqa # try our best to email pass - cur.execute( - "UPDATE campaign.fundrazr_daklapack_orders " - "SET tracking_sent = true " - "WHERE fundrazr_transaction_perk_id = %s " - "AND dak_order_id = %s", - (r['fundrazr_transaction_perk_id'], r['dak_order_id']) - ) + def _fulfill_kit(self, row, quantity, subscription_id): + projects = \ + self._campaign_id_to_projects(row['campaign_id']) + + if "account_id" in row and row['account_id'] is not None: + country = pycountry.countries.get( + alpha_2=row['a_country'] + ) + country_name = country.name + + address_dict = { + "firstName": row['a_first_name'], + "lastName": row['a_last_name'], + "address1": row['a_address_1'], + "insertion": "", + "address2": "", + "postalCode": row['a_postal_code'], + "city": row['a_city'], + "state": row['a_state'], + "country": country_name, + "countryCode": row['a_country'], + "phone": row['a_phone'] + } + else: + country = pycountry.countries.get( + alpha_2=row['country'] + ) + country_name = country.name + + address_dict = { + "firstName": row['first_name'], + "lastName": row['last_name'], + "address1": row['address_1'], + "insertion": "", + "address2": row['address_2'], + "postalCode": row['postal_code'], + "city": row['city'], + "state": row['state'], + "country": country_name, + "countryCode": row['country'], + "phone": row['phone'] + } - def _fulfill_kit(self, ftp_id, projects, dak_article_code, quantity, - address_dict): # TODO: If we expand automated perk fulfillment beyond the US, we'll # need to handle shipping provider/type more elegantly. daklapack_order = { "project_ids": projects, - "article_code": dak_article_code, + "article_code": row['dak_article_code'], "address": address_dict, "quantity": quantity, "shipping_provider": FEDEX_PROVIDER, @@ -388,26 +351,69 @@ def _fulfill_kit(self, ftp_id, projects, dak_article_code, quantity, ") VALUES (" "%s, %s, %s" ")", - (ftp_id, result['order_id'], False) + (row['ftp_id'], result['order_id'], False) ) + + # If this is the first kit of a subscription, + # we mark it as both scheduled and fulfilled + if subscription_id is not None: + cur_date = datetime.now() + cur_date = cur_date.strftime("%Y-%m-%d") + self._schedule_kit(subscription_id, + cur_date, + row['dak_article_code'], + True) + return True, result['order_id'] - def _fulfill_ffq(self, ftp_id): + def _fulfill_ffq(self, ftp_id, template, email, first_name, + subscription_id=None): code = ActivationCode.generate_code() with self._transaction.cursor() as cur: + # Insert the newly created registration code cur.execute( "INSERT INTO campaign.ffq_registration_codes (" "ffq_registration_code" ") VALUES (%s)", (code,) ) + + # Log the registration code as a fulfillment of a given FTP cur.execute( "INSERT INTO campaign.fundrazr_ffq_codes (" "fundrazr_transaction_perk_id, ffq_registration_code" ") VALUES (%s, %s)", (ftp_id, code) ) - return code + + # If this is the first FFQ of a subscription, + # we mark it as both scheduled and fulfilled + if subscription_id is not None: + cur_date = datetime.now() + cur_date = cur_date.strftime("%Y-%m-%d") + self._schedule_ffq(subscription_id, cur_date, + True) + + email_error = None + try: + send_email( + email, + template, + { + "first_name": first_name, + "registration_code": code, + "interface_endpoint": + SERVER_CONFIG["interface_endpoint"] + }, + EN_US + ) + except Exception as e: # noqa + # if the email fails, we'll log why but continue executing + email_error = f"FFQ registration code email failed "\ + f"for ftp_id={ftp_id} and code={code} with "\ + f"the following: {repr(e)}" + + return email_error def _schedule_kit(self, subscription_id, fulfillment_date, dak_article_code, fulfilled): @@ -589,3 +595,7 @@ def _future_fulfillment_date(self, spacing_number, spacing_unit, "fulfillment_spacing_unit") return new_date.strftime("%Y-%m-%d") + + def _is_subscription(self, perk): + return (perk['ffq_quantity'] > 1 or perk['kit_quantity'] > 1) and \ + (perk['fulfillment_spacing_number'] > 0) diff --git a/microsetta_private_api/repo/tests/test_perk_fulfillment_repo.py b/microsetta_private_api/repo/tests/test_perk_fulfillment_repo.py index b815d0d2e..3107e0952 100644 --- a/microsetta_private_api/repo/tests/test_perk_fulfillment_repo.py +++ b/microsetta_private_api/repo/tests/test_perk_fulfillment_repo.py @@ -14,6 +14,7 @@ from microsetta_private_api.model.daklapack_order import DaklapackOrder from microsetta_private_api.repo.account_repo import AccountRepo from microsetta_private_api.repo.admin_repo import AdminRepo +from microsetta_private_api.model.subscription import FULFILLMENT_ACCOUNT_ID ADDRESS1 = Address( @@ -115,7 +116,7 @@ ) DUMMY_ORDER_ID = str(uuid.uuid4()) -SUBMITTER_ID = "000fc4cd-8fa4-db8b-e050-8a800c5d81b7" +SUBMITTER_ID = FULFILLMENT_ACCOUNT_ID SUBMITTER_NAME = "demo demo" PROJECT_IDS = [1, ] DUMMY_DAKLAPACK_ORDER = { @@ -271,9 +272,6 @@ def test_process_pending_fulfillments_one_kit_succeed( pfr = PerkFulfillmentRepo(t) res = pfr.process_pending_fulfillments() - # res is a list of errors, which should be 0 - self.assertEqual(len(res), 0) - cur = t.cursor() # Confirm that the order populated into fundrazr_daklapack_orders @@ -311,7 +309,7 @@ def test_process_pending_fulfillments_one_kit_fail( res = pfr.process_pending_fulfillments() # res is a list of errors, which should be 1 - self.assertEqual(len(res), 1) + self.assertNotEqual(len(res), 0) @patch("microsetta_private_api.repo.interested_user_repo.verify_address") def test_process_pending_fulfillments_one_ffq(self, @@ -392,9 +390,6 @@ def test_transaction_one_subscription( pfr = PerkFulfillmentRepo(t) res = pfr.process_pending_fulfillments() - # Confirm the fulfillment processed - self.assertEqual(len(res), 0) - cur = t.dict_cursor() # We need to grab the subscription ID @@ -503,9 +498,6 @@ def test_get_subscription_by_id( pfr = PerkFulfillmentRepo(t) res = pfr.process_pending_fulfillments() - # Confirm the fulfillment processed - self.assertEqual(len(res), 0) - cur = t.dict_cursor() # We need to grab the subscription ID