Skip to content

Commit

Permalink
Perk Fulfillment (#491)
Browse files Browse the repository at this point in the history
* WIP: Perk fulfillment code

* Lint fixes

* Lint fixes

* Lint fixes

* Lint fixes

* Test fix

* Email templates for perk fulfillment

* Test adjustment

* Updates to address comments/requests

* Lint fixes

* Addressing PR comments

* Lint fixes

* Disabling Daklapack ordering & celery tasks for testing

* Get transactions on celery start

* Route invalid address emails to myself temporarily

* Test perks without fulfillment details

* Test new perk fulfillment

* Test shipping updates

* Fix shipping update query

* Fix shipping update query

* Email adjustments

* Re-test get fundrazr transactions to verify email fixes

* Re-test fulfillment to verify email fixes

* Test date limiting of address verification

* Test date limiting of address verification

* Test date limiting of address verification

* Debug not getting new transaction

* Re-test perk fulfillment w/ Daklapack ordering

* Cleaning up from testing

* Lint fixes

* Add unit test for shipping updates

* Lint fixes

* Run Celery tasks on startup

* Only send Fundrazr email if transactions > 0

* Add email logging to perk fulfillment

* Resolve db patch filename conflict

* Test email logging

* Lint

* Fix subscription fulfillment bug + expand unit tests

* Lint

* Update perk_fulfillment_repo.py

* Update celery_utils.py

* Update server_config.json

* Update admin_impl.py

* Update test_perk_fulfillment_repo.py

* Update subscription.py
  • Loading branch information
cassidysymons authored Jan 5, 2023
1 parent e499dad commit 06344e9
Show file tree
Hide file tree
Showing 20 changed files with 3,186 additions and 52 deletions.
15 changes: 15 additions & 0 deletions microsetta_private_api/admin/admin_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,21 @@ def create_daklapack_orders(body, token_info):
return response


# We need an internal wrapper to create orders based on contributions coming
# from Fundrazr without authenticating an admin user.
# Do NOT expose an API endpoint for this.
def create_daklapack_order_internal(order_dict):
# Since we've established the consent dummy as a stable account across
# dev, staging, and production, we'll continue to use that internally
with Transaction() as t:
account_repo = AccountRepo(t)
order_dict[SUBMITTER_ACCT_KEY] = account_repo.get_account(
SERVER_CONFIG['fulfillment_account_id'])

result = _create_daklapack_order(order_dict)
return result


def _create_daklapack_order(order_dict):
order_dict[ORDER_ID_KEY] = str(uuid.uuid4())

Expand Down
32 changes: 32 additions & 0 deletions microsetta_private_api/admin/email_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,38 @@ class EmailMessage(Enum):
EventType.EMAIL,
EventSubtype.EMAIL_SUBMIT_INTEREST_CONFIRMATION
)
thank_you_with_kit = (
gettext(
"Registration code & kit update!"),
"email/thank_you_with_kit.jinja2",
("first_name", "registration_code", "interface_endpoint"),
EventType.EMAIL,
EventSubtype.EMAIL_THANK_YOU_WITH_KIT
)
thank_you_no_kit = (
gettext(
"Your questionnaire is ready!"),
"email/thank_you_no_kit.jinja2",
("first_name", "registration_code", "interface_endpoint"),
EventType.EMAIL,
EventSubtype.EMAIL_THANK_YOU_NO_KIT
)
kit_tracking_number = (
gettext(
"Your kit is on its way!"),
"email/kit_tracking_number.jinja2",
("first_name", "tracking_number"),
EventType.EMAIL,
EventSubtype.EMAIL_KIT_TRACKING_NUMBER
)
subscription_ffq_code = (
gettext(
"Registration code & kit update!"),
"email/subscription_ffq_code.jinja2",
("first_name", "tracking_number", "interface_endpoint"),
EventType.EMAIL,
EventSubtype.EMAIL_SUBSCRIPTION_FFQ_CODE
)

def __init__(self, subject, html, required, event_type, event_sub):
self.subject = subject
Expand Down
12 changes: 12 additions & 0 deletions microsetta_private_api/api/_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from microsetta_private_api.repo.account_repo import AccountRepo
from microsetta_private_api.repo.transaction import Transaction
from microsetta_private_api.config_manager import SERVER_CONFIG
from microsetta_private_api.repo.perk_fulfillment_repo import\
PerkFulfillmentRepo


def find_accounts_for_login(token_info):
Expand Down Expand Up @@ -66,6 +68,16 @@ def register_account(body, token_info):
acct_repo = AccountRepo(t)
acct_repo.create_account(account_obj)
new_acct = acct_repo.get_account(new_acct_id)

# Check for unclaimed subscriptions attached to the email address
pfr = PerkFulfillmentRepo(t)
subscription_ids = pfr.get_unclaimed_subscriptions_by_email(
new_acct.email
)
# If we find any, claim them
for sub_id in subscription_ids:
pfr.claim_unclaimed_subscription(sub_id, new_acct_id)

t.commit()

response = jsonify(new_acct.to_api())
Expand Down
22 changes: 21 additions & 1 deletion microsetta_private_api/celery_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,32 @@ def __call__(self, *args, **kwargs):
},
"poll_daklapack_orders": {
"task": "microsetta_private_api.admin.daklapack_polling.poll_dak_orders", # noqa
"schedule": 60 * 60 * 24 # every 24 hours
"schedule": 60 * 60 * 4 # every 4 hours
},
"update_qiita_metadata": {
"task": "microsetta_private_api.tasks.update_qiita_metadata", # noqa
"schedule": 60 * 60 * 24 # every 24 hours
},
"pull_fundrazr_transactions": {
"task": "microsetta_private_api.util.fundrazr.get_fundrazr_transactions", # noqa
"schedule": 60 * 60 # every hour
},
"fulfill_new_transactions": {
"task": "microsetta_private_api.util.perk_fulfillment.fulfill_new_transactions", # noqa
"schedule": 60 * 60 # every hour
},
"fulfill_subscriptions": {
"task": "microsetta_private_api.util.perk_fulfillment.process_subscription_fulfillments", # noqa
"schedule": 60 * 60 * 24 # every 24 hours
},
"check_shipping_updates": {
"task": "microsetta_private_api.util.perk_fulfillment.check_shipping_updates", # noqa
"schedule": 60 * 60 * 4 # every 4 hours
},
"perks_without_fulfillment_details": {
"task": "microsetta_private_api.util.perk_fulfillment.perks_without_fulfillment_details", # noqa
"schedule": 60 * 60 * 24 # every 24 hours
},
# "fetch_ffqs": {
# "task": "microsetta_private_api.util.vioscreen.fetch_ffqs",
# "schedule": 60 * 60 * 24 # every 24 hours
Expand Down
11 changes: 9 additions & 2 deletions microsetta_private_api/celery_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
from microsetta_private_api.celery_utils import celery, init_celery
from microsetta_private_api.util.vioscreen import refresh_headers
from microsetta_private_api.admin.daklapack_polling import poll_dak_orders
from microsetta_private_api.tasks import update_qiita_metadata
# from microsetta_private_api.util.fundrazr import get_fundrazr_transactions
from microsetta_private_api.util.perk_fulfillment import check_shipping_updates
# from microsetta_private_api.tasks import update_qiita_metadata
init_celery(celery, app.app)

# Run any celery tasks that require initialization on worker start
refresh_headers.delay() # Initialize the vioscreen task with a token
poll_dak_orders.delay() # check for orders
update_qiita_metadata.delay() # run Qiita metadata push
# get_fundrazr_transactions.delay() # check for new transactions
check_shipping_updates.delay() # check for tracking updates
# update_qiita_metadata.delay() # run Qiita metadata push

# Disabling Qiita metadata push until we have survey changes in place and
# are ready to test. - Cassidy 2022-12-01
4 changes: 2 additions & 2 deletions microsetta_private_api/client/tests/test_fundrazr.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ def test_payments(self):
def test_campaigns(self):
obs = self.c.campaigns()

# staging has two campaigns, let's assume that's stable...
self.assertEqual(len(obs), 2)
# staging has three campaigns, let's assume that's stable...
self.assertEqual(len(obs), 3)
self.assertTrue(len(obs[0].items) > 0)

@skipIf(SERVER_CONFIG['fundrazr_url'] in ('', 'fundrazr_url_placeholder'),
Expand Down
106 changes: 106 additions & 0 deletions microsetta_private_api/db/patches/0110.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
-- Add a flag to the campaign.fundrazr_transaction_perk table to reflect whether it has been processed
ALTER TABLE campaign.fundrazr_transaction_perk ADD COLUMN processed BOOLEAN NOT NULL DEFAULT FALSE;

-- Add a flag to the campaign.fundrazr_daklapack_orders table to reflect whether we've sent out a tracking number
ALTER TABLE campaign.fundrazr_daklapack_orders ADD COLUMN tracking_sent BOOLEAN NOT NULL DEFAULT FALSE;

-- The campaign.fundrazr_perk_to_daklapack_article table hasn't been used yet, so it's safe to drop.
DROP TABLE campaign.fundrazr_perk_to_daklapack_article;

-- Since our model has changed to include perks that are FFQ-only and subscriptions,
-- we're going to retool the table to better reflect how perks function.
-- For subscriptions, we'll utilize the fulfillment_spacing_* columns to control scheduling.
-- E.g., fulfillment_spacing_number = 3 and fulfillment_spacing_unit = 'months' will schedule quarterly orders.
-- If we decide to offer perks with multiple kits shipped at once in the future, fulfillment_spacing_number = 0 can be used to reflect this behavior.
CREATE TYPE FULFILLMENT_SPACING_UNIT AS ENUM ('days', 'months');
CREATE TABLE campaign.fundrazr_perk_fulfillment_details (
perk_id VARCHAR NOT NULL PRIMARY KEY,
ffq_quantity INTEGER NOT NULL,
kit_quantity INTEGER NOT NULL,
dak_article_code VARCHAR, -- Must be nullable, as not all perks include a kit
fulfillment_spacing_number INTEGER NOT NULL,
fulfillment_spacing_unit FULFILLMENT_SPACING_UNIT,
CONSTRAINT fk_perk_to_dak FOREIGN KEY (dak_article_code) REFERENCES barcodes.daklapack_article (dak_article_code)
);

INSERT INTO campaign.transaction_source_to_campaign
SELECT '57xV2' AS remote_campaign_id, campaign_id AS internal_campaign_id, 'usd' AS currency
FROM campaign.campaigns
WHERE title='The Microsetta Initiative';

-- The API will pull down perks automatically, but we need it to exist so we can add the fullfilment info,
-- so we're just going to insert them here

-- Perk values for Fundrazr's production environment
INSERT INTO campaign.fundrazr_perk
(id, remote_campaign_id, title, price)
VALUES ('3QeVd', '4Tqx5', 'Analyze Your Nutrition', 20),
('3QeW6', '4Tqx5', 'Explore Your Microbiome', 180),
('0QeXa', '4Tqx5', 'Follow Your Gut', 720);

-- Perk values for Fundrazr's staging environment
INSERT INTO campaign.fundrazr_perk
(id, remote_campaign_id, title, price)
VALUES ('13lja', '57xV2', 'Analyze Your Nutrition', 20),
('93lk8', '57xV2', 'Explore Your Microbiome', 180),
('13ll7', '57xV2', 'Follow Your Gut', 720);

-- Insert the fulfillment info for the perks we're offering
-- Production perks
INSERT INTO campaign.fundrazr_perk_fulfillment_details
(perk_id, ffq_quantity, kit_quantity, dak_article_code, fulfillment_spacing_number, fulfillment_spacing_unit)
VALUES ('3QeVd', 1, 0, NULL, 0, NULL),
('3QeW6', 1, 1, '3510005E', 0, NULL),
('0QeXa', 4, 4, '3510005E', 3, 'months');
-- Staging perks
INSERT INTO campaign.fundrazr_perk_fulfillment_details
(perk_id, ffq_quantity, kit_quantity, dak_article_code, fulfillment_spacing_number, fulfillment_spacing_unit)
VALUES ('13lja', 1, 0, NULL, 0, NULL),
('93lk8', 1, 1, '3510005E', 0, NULL),
('13ll7', 4, 4, '3510005E', 3, 'months');

-- Both the subscriptions and subscriptions_fulfillment tables will have cancelled flags to create
-- an audit trail in the event someone contacts us to cancel scheduled shipments.
-- We're storing the flag at both levels so we can track what portion of a subscription was actually sent.
-- We say "No refunds" but it would be good to have the granular data on what people received, just in case.
CREATE TABLE campaign.subscriptions (
subscription_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
account_id UUID, -- Must be nullable in case someone contributes to receive a subscription before creating their account
transaction_id VARCHAR NOT NULL,
fundrazr_transaction_perk_id UUID NOT NULL,
cancelled BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT fk_account_id FOREIGN KEY (account_id) REFERENCES ag.account (id),
CONSTRAINT fk_transaction_id FOREIGN KEY (transaction_id) REFERENCES campaign.transaction (id),
CONSTRAINT fk_ftp_id FOREIGN KEY (fundrazr_transaction_perk_id) REFERENCES campaign.fundrazr_transaction_perk (id)
);

-- Participants can alter shipment dates, but only once per shipment.
-- The fulfillment_date_changed column will manage this.
CREATE TYPE FULFILLMENT_TYPE AS ENUM ('ffq', 'kit');
CREATE TABLE campaign.subscriptions_fulfillment (
fulfillment_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
subscription_id UUID NOT NULL,
fulfillment_type FULFILLMENT_TYPE NOT NULL,
dak_article_code VARCHAR, -- Must be nullable, as not all perks include a kit
fulfillment_date DATE,
fulfillment_date_changed BOOLEAN NOT NULL DEFAULT FALSE,
fulfilled BOOLEAN NOT NULL DEFAULT FALSE,
cancelled BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT fk_subscription_id FOREIGN KEY (subscription_id) REFERENCES campaign.subscriptions (subscription_id),
CONSTRAINT fk_dak_article_code FOREIGN KEY (dak_article_code) REFERENCES barcodes.daklapack_article (dak_article_code)
);

-- FFQs will have a registration code (similar to the old activation code) moving forward,
-- although it will not be tied to an email address.
CREATE TABLE campaign.ffq_registration_codes (
ffq_registration_code VARCHAR PRIMARY KEY,
registration_code_used TIMESTAMP -- Nullable as null = unused code
);

-- Create a record of the fulfillment of FFQ codes relative to a transaction/perk combination.
CREATE TABLE campaign.fundrazr_ffq_codes (
fundrazr_transaction_perk_id UUID NOT NULL,
ffq_registration_code VARCHAR NOT NULL,
CONSTRAINT fk_ftp_id FOREIGN KEY (fundrazr_transaction_perk_id) REFERENCES campaign.fundrazr_transaction_perk (id),
CONSTRAINT fk_ffq_code FOREIGN KEY (ffq_registration_code) REFERENCES campaign.ffq_registration_codes (ffq_registration_code)
);
8 changes: 8 additions & 0 deletions microsetta_private_api/model/log_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ class EventSubtype(Enum):
EMAIL_ADDRESS_INVALID = "address_invalid"
# for confirmation emails of interested user signups
EMAIL_SUBMIT_INTEREST_CONFIRMATION = "submit_interest_confirmation"
# Thank you for Fundrazr contribution - kit or subscription
EMAIL_THANK_YOU_WITH_KIT = "thank_you_with_kit"
# Thank you for Fundrazr contribution - FFQ only
EMAIL_THANK_YOU_NO_KIT = "thank_you_no_kit"
# Send tracking number for newly shipped kit
EMAIL_KIT_TRACKING_NUMBER = "kit_tracking_number"
# When next subscription fulfillment occurs, send FFQ code
EMAIL_SUBSCRIPTION_FFQ_CODE = "subscription_ffq_code"


class LogEvent(ModelBase):
Expand Down
21 changes: 21 additions & 0 deletions microsetta_private_api/model/subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from microsetta_private_api.model.model_base import ModelBase


class Subscription(ModelBase):
def __init__(self, **kwargs):
# subscription_id won't exist yet on new subscriptions
self.subscription_id = kwargs.get('subscription_id')

# absolute minimum fields required for a subscription
self.transaction_id = kwargs['transaction_id']

# remaining fields are either optional or auto-created later
self.account_id = kwargs.get('account_id')
self.cancelled = kwargs.get('cancelled', False)

def to_api(self):
return self.__dict__.copy()

@classmethod
def from_dict(cls, values_dict):
return cls(**values_dict)
Loading

0 comments on commit 06344e9

Please sign in to comment.