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

Perk Fulfillment #491

Merged
merged 46 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a8b96a7
WIP: Perk fulfillment code
cassidysymons Nov 23, 2022
d5aa7aa
Lint fixes
cassidysymons Nov 23, 2022
27ce42f
Lint fixes
cassidysymons Nov 23, 2022
38bc9ba
Lint fixes
cassidysymons Nov 23, 2022
da26933
Lint fixes
cassidysymons Nov 23, 2022
987699e
Test fix
cassidysymons Nov 23, 2022
86e44e9
Email templates for perk fulfillment
cassidysymons Nov 23, 2022
4d9c6d9
Test adjustment
cassidysymons Nov 23, 2022
b551efb
Updates to address comments/requests
cassidysymons Nov 28, 2022
3e1362a
Lint fixes
cassidysymons Nov 28, 2022
6874524
Addressing PR comments
cassidysymons Nov 30, 2022
6611466
Lint fixes
cassidysymons Nov 30, 2022
c6fb09f
Disabling Daklapack ordering & celery tasks for testing
cassidysymons Nov 30, 2022
bed3590
Get transactions on celery start
cassidysymons Nov 30, 2022
cdd81d6
Route invalid address emails to myself temporarily
cassidysymons Nov 30, 2022
28434c7
Test perks without fulfillment details
cassidysymons Nov 30, 2022
e908873
Test new perk fulfillment
cassidysymons Nov 30, 2022
c340f37
Test shipping updates
cassidysymons Nov 30, 2022
30d3431
Fix shipping update query
cassidysymons Nov 30, 2022
1a1b704
Fix shipping update query
cassidysymons Nov 30, 2022
6b6c6ff
Email adjustments
cassidysymons Nov 30, 2022
f4178d7
Re-test get fundrazr transactions to verify email fixes
cassidysymons Nov 30, 2022
93a7cd0
Re-test fulfillment to verify email fixes
cassidysymons Nov 30, 2022
aae78a6
Test date limiting of address verification
cassidysymons Nov 30, 2022
69185a8
Test date limiting of address verification
cassidysymons Nov 30, 2022
4b36fb7
Test date limiting of address verification
cassidysymons Dec 1, 2022
05dcbd3
Debug not getting new transaction
cassidysymons Dec 1, 2022
8d01497
Re-test perk fulfillment w/ Daklapack ordering
cassidysymons Dec 1, 2022
b611ba1
Cleaning up from testing
cassidysymons Dec 1, 2022
b3e3e2d
Lint fixes
cassidysymons Dec 1, 2022
f7f4b18
Add unit test for shipping updates
cassidysymons Dec 5, 2022
e7fd999
Lint fixes
cassidysymons Dec 5, 2022
b7226ed
Run Celery tasks on startup
cassidysymons Dec 6, 2022
aae0999
Only send Fundrazr email if transactions > 0
cassidysymons Dec 6, 2022
fcbc69a
Add email logging to perk fulfillment
cassidysymons Dec 31, 2022
2074d8d
Resolve db patch filename conflict
cassidysymons Dec 31, 2022
f53a918
Test email logging
cassidysymons Dec 31, 2022
3b22343
Lint
cassidysymons Dec 31, 2022
ad74bd3
Fix subscription fulfillment bug + expand unit tests
cassidysymons Jan 1, 2023
e378b17
Lint
cassidysymons Jan 1, 2023
7ad9787
Update perk_fulfillment_repo.py
cassidysymons Jan 5, 2023
e9189e6
Update celery_utils.py
cassidysymons Jan 5, 2023
db0efaf
Update server_config.json
cassidysymons Jan 5, 2023
3905f2f
Update admin_impl.py
cassidysymons Jan 5, 2023
fdc6fef
Update test_perk_fulfillment_repo.py
cassidysymons Jan 5, 2023
46671e1
Update subscription.py
cassidysymons Jan 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions microsetta_private_api/admin/admin_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -542,6 +543,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(
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
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 hour
},
"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
85 changes: 85 additions & 0 deletions microsetta_private_api/db/patches/0108.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
-- 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)
);

-- 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
INSERT INTO campaign.fundrazr_perk
(id, remote_campaign_id, title, price)
VALUES ('3QeVd', '4Tqx5', 'Analyze Your Nutrition', 20),
cassidysymons marked this conversation as resolved.
Show resolved Hide resolved
('3QeW6', '4Tqx5', 'Explore Your Microbiome', 180),
('0QeXa', '4Tqx5', 'Follow Your Gut', 720);

-- Insert the fulfillment info for the perks we're offering
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');

-- 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 (
wasade marked this conversation as resolved.
Show resolved Hide resolved
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
23 changes: 23 additions & 0 deletions microsetta_private_api/model/subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from microsetta_private_api.model.model_base import ModelBase

FULFILLMENT_ACCOUNT_ID = "000fc4cd-8fa4-db8b-e050-8a800c5d81b7"
cassidysymons marked this conversation as resolved.
Show resolved Hide resolved


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)
7 changes: 4 additions & 3 deletions microsetta_private_api/repo/campaign_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,14 +584,15 @@ def _add_transaction_fundrazr(self, payment, interested_user_id):
data)

if items is not None:
inserts = [(payment.transaction_id, i.id, i.quantity)
inserts = [(payment.transaction_id, i.id, i.quantity, False)
for i in items]

try:
cur.executemany("""INSERT INTO
campaign.fundrazr_transaction_perk
(transaction_id, perk_id, quantity)
VALUES (%s, %s, %s)""",
(transaction_id, perk_id, quantity,
processed)
VALUES (%s, %s, %s, %s)""",
inserts)
except psycopg2.errors.ForeignKeyViolation:
# this would indicate a synchronization issue, where
Expand Down
Loading