Skip to content

Commit

Permalink
Merge pull request #4392 from MidwestFurryFandom/dealer-reg-2024
Browse files Browse the repository at this point in the history
Import fixes for receipts, cancelling badges, and promo codes from MFF
  • Loading branch information
kitsuta committed Aug 20, 2024
2 parents 5eb0cbd + e533b34 commit acdffea
Show file tree
Hide file tree
Showing 40 changed files with 359 additions and 192 deletions.
61 changes: 61 additions & 0 deletions alembic/versions/9fb2b1c462c2_fix_deleting_groups_with_leaders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Fix deleting groups with leaders
Revision ID: 9fb2b1c462c2
Revises: f1a8794a398f
Create Date: 2024-08-04 18:46:15.939539
"""


# revision identifiers, used by Alembic.
revision = '9fb2b1c462c2'
down_revision = 'f1a8794a398f'
branch_labels = None
depends_on = None

from alembic import op
import sqlalchemy as sa



try:
is_sqlite = op.get_context().dialect.name == 'sqlite'
except Exception:
is_sqlite = False

if is_sqlite:
op.get_context().connection.execute('PRAGMA foreign_keys=ON;')
utcnow_server_default = "(datetime('now', 'utc'))"
else:
utcnow_server_default = "timezone('utc', current_timestamp)"

def sqlite_column_reflect_listener(inspector, table, column_info):
"""Adds parenthesis around SQLite datetime defaults for utcnow."""
if column_info['default'] == "datetime('now', 'utc')":
column_info['default'] = utcnow_server_default

sqlite_reflect_kwargs = {
'listeners': [('column_reflect', sqlite_column_reflect_listener)]
}

# ===========================================================================
# HOWTO: Handle alter statements in SQLite
#
# def upgrade():
# if is_sqlite:
# with op.batch_alter_table('table_name', reflect_kwargs=sqlite_reflect_kwargs) as batch_op:
# batch_op.alter_column('column_name', type_=sa.Unicode(), server_default='', nullable=False)
# else:
# op.alter_column('table_name', 'column_name', type_=sa.Unicode(), server_default='', nullable=False)
#
# ===========================================================================


def upgrade():
op.drop_constraint('fk_leader', 'group', type_='foreignkey')
op.create_foreign_key('fk_leader', 'group', 'attendee', ['leader_id'], ['id'], ondelete='SET NULL', use_alter=True)


def downgrade():
op.drop_constraint('fk_leader', 'group', type_='foreignkey')
op.create_foreign_key('fk_leader', 'group', 'attendee', ['leader_id'], ['id'])
2 changes: 1 addition & 1 deletion uber/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ def update(self, id, params):
and c.VOLUNTEER_RIBBON not in attendee.ribbon_ints and 'paid' not in params:
attendee.paid = c.NEED_NOT_PAY

return attendee.id
return attendee.id


@all_api_auth('api_read')
Expand Down
4 changes: 2 additions & 2 deletions uber/automated_emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ def generic_placeholder(a): return a.placeholder and (not deferred_attendee_plac
StopsEmailFixture(
'Last chance to personalize your {EVENT_NAME} ({EVENT_DATE}) badge',
'personalized_badges/volunteers.txt',
lambda a: (a.staffing and a.badge_type in c.PREASSIGNED_BADGE_TYPES and a.placeholder
lambda a: (a.staffing and a.has_personalized_badge and a.placeholder
and a.badge_type != c.CONTRACTOR_BADGE),
when=days_before(7, c.PRINTED_BADGE_DEADLINE),
ident='volunteer_personalized_badge_reminder')
Expand All @@ -734,7 +734,7 @@ def generic_placeholder(a): return a.placeholder and (not deferred_attendee_plac
Attendee,
'Personalized {EVENT_NAME} ({EVENT_DATE}) badges will be ordered next week',
'personalized_badges/reminder.txt',
lambda a: a.badge_type in c.PREASSIGNED_BADGE_TYPES and not a.placeholder,
lambda a: a.has_personalized_badge and not a.placeholder,
when=days_before(7, c.PRINTED_BADGE_DEADLINE),
ident='personalized_badge_reminder')

Expand Down
5 changes: 4 additions & 1 deletion uber/configspec.ini
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ accessibility_services_enabled = boolean(default=False)
# during preregistration, False to hide the "Confirm Email" field altogether.
prereg_confirm_email_enabled = boolean(default=False)

# If this is set to True, prereg_open is not displayed to attendees.
hide_prereg_open_date = boolean(default=True)

# We expose some basic services, like an attendee lookup via jsonrpc.
api_enabled = boolean(default=True)

Expand Down Expand Up @@ -1414,7 +1417,7 @@ imported_status = string(default="Imported")
new_status = string(default="New")
completed_status = string(default="Complete")
invalid_status = string(default="Invalid")
refunded_status = string(default="Refunded")
refunded_status = string(default="Refunded/Cancelled")
deferred_status = string(default="Deferred")
watched_status = string(default="On Hold")
not_attending = string(default="Not Attending")
Expand Down
8 changes: 4 additions & 4 deletions uber/forms/attendee.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ def get_optional_fields(self, attendee, is_admin=False):
if is_admin or not attendee.needs_pii_consent and attendee.badge_status != c.PENDING_STATUS:
optional_list.append('confirm_email')

if attendee.badge_type not in c.PREASSIGNED_BADGE_TYPES or (c.PRINTED_BADGE_DEADLINE
and c.AFTER_PRINTED_BADGE_DEADLINE):
if not attendee.has_personalized_badge or (c.PRINTED_BADGE_DEADLINE
and c.AFTER_PRINTED_BADGE_DEADLINE):
optional_list.append('badge_printed_name')

if self.same_legal_name.data:
Expand Down Expand Up @@ -502,8 +502,8 @@ class CheckInForm(MagForm):
def get_optional_fields(self, attendee, is_admin=False):
optional_list = super().get_optional_fields(attendee, is_admin)

if attendee.badge_type not in c.PREASSIGNED_BADGE_TYPES or (c.PRINTED_BADGE_DEADLINE and
c.AFTER_PRINTED_BADGE_DEADLINE):
if not attendee.has_personalized_badge or (c.PRINTED_BADGE_DEADLINE and
c.AFTER_PRINTED_BADGE_DEADLINE):
optional_list.append('badge_printed_name')

return optional_list
Expand Down
8 changes: 8 additions & 0 deletions uber/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ def email_to_address(self):
automated emails -- override this instead.
"""
return self.email

def cc_emails_for_ident(self, ident=''):
# A list of emails to carbon-copy for an automated email with a particular ident
return

def bcc_emails_for_ident(self, ident=''):
# A list of emails to blind-carbon-copy for an automated email with a particular ident
return

@property
def gets_emails(self):
Expand Down
20 changes: 12 additions & 8 deletions uber/models/attendee.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ def badge_cost_with_promo_code(self):
return self.calculate_badge_cost(use_promo_code=True)

def calculate_badge_cost(self, use_promo_code=False, include_price_override=True):
if self.paid == c.NEED_NOT_PAY:
if self.paid == c.NEED_NOT_PAY or self.badge_status == c.NOT_ATTENDING:
return 0
elif self.overridden_price is not None and include_price_override:
return self.overridden_price
Expand Down Expand Up @@ -1024,17 +1024,16 @@ def valid_placeholder(self):
@hybrid_property
def is_valid(self):
return self.badge_status not in [c.PENDING_STATUS, c.AT_DOOR_PENDING_STATUS, c.INVALID_STATUS,
c.IMPORTED_STATUS, c.INVALID_GROUP_STATUS]
c.IMPORTED_STATUS, c.INVALID_GROUP_STATUS, c.REFUNDED_STATUS]

@is_valid.expression
def is_valid(cls):
return not_(cls.badge_status.in_([c.PENDING_STATUS, c.AT_DOOR_PENDING_STATUS, c.INVALID_STATUS,
c.IMPORTED_STATUS, c.INVALID_GROUP_STATUS]))
c.IMPORTED_STATUS, c.INVALID_GROUP_STATUS, c.REFUNDED_STATUS]))

@hybrid_property
def has_or_will_have_badge(self):
return self.is_valid and self.badge_status not in [c.REFUNDED_STATUS, c.NOT_ATTENDING,
c.UNAPPROVED_DEALER_STATUS]
return self.is_valid and self.badge_status not in [c.NOT_ATTENDING, c.UNAPPROVED_DEALER_STATUS]

@has_or_will_have_badge.expression
def has_or_will_have_badge(cls):
Expand Down Expand Up @@ -1143,18 +1142,18 @@ def cannot_abandon_badge_reason(self):
)

reason = ""
if self.paid == c.NEED_NOT_PAY and not self.in_promo_code_group:
if self.paid == c.NEED_NOT_PAY and not self.promo_code:
reason = "You cannot abandon a comped badge."
elif self.is_group_leader and self.group.is_valid:
reason = f"As a leader of a group, you cannot {'abandon' if not self.group.cost else 'refund'} your badge."
elif self.amount_paid:
reason = self.cannot_self_service_refund_reason

if reason:
return reason + "Please {} contact us at {}{}.".format(
return reason + " Please {} contact us at {}{}.".format(
"transfer your badge instead or" if self.is_transferable else "",
email_only(c.REGDESK_EMAIL),
" for a refund" if c.SELF_SERVICE_REFUNDS_OPEN else "")
" to cancel your badge.")

@property
def cannot_self_service_refund_reason(self):
Expand Down Expand Up @@ -2249,6 +2248,11 @@ def imported_single_badges(self):
@property
def imported_group_badges(self):
return [attendee for attendee in self.imported_attendees if attendee.group]

@property
def imported_group_leaders(self):
return [attendee for attendee in self.imported_attendees
if attendee.group and attendee.id == attendee.group.leader_id]

@property
def pending_attendees(self):
Expand Down
4 changes: 2 additions & 2 deletions uber/models/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,8 @@ def send_to(self, model_instance, delay=True, raise_errors=False):
self.render_template(self.body, data),
self.format,
model=model_instance.to_dict('id'),
cc=self.cc,
bcc=self.bcc,
cc=self.cc or model_instance.cc_emails_for_ident(self.ident),
bcc=self.bcc or model_instance.bcc_emails_for_ident(self.ident),
ident=self.ident,
automated_email=self.to_dict('id'))
return True
Expand Down
3 changes: 2 additions & 1 deletion uber/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ class Group(MagModel, TakesPaymentMixin):
status = Column(Choice(c.DEALER_STATUS_OPTS), default=c.UNAPPROVED, admin_only=True)
registered = Column(UTCDateTime, server_default=utcnow(), default=lambda: datetime.now(UTC))
approved = Column(UTCDateTime, nullable=True)
leader_id = Column(UUID, ForeignKey('attendee.id', use_alter=True, name='fk_leader'), nullable=True)
leader_id = Column(UUID, ForeignKey('attendee.id', use_alter=True, name='fk_leader', ondelete='SET NULL'),
nullable=True)
creator_id = Column(UUID, ForeignKey('attendee.id'), nullable=True)

creator = relationship(
Expand Down
2 changes: 1 addition & 1 deletion uber/models/promo_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ def normalized_code(cls):

@property
def valid_used_by(self):
return [attendee for attendee in self.used_by if attendee.is_valid]
return list(set([attendee for attendee in self.used_by if attendee.is_valid]))

@property
def uses_allowed_str(self):
Expand Down
9 changes: 5 additions & 4 deletions uber/models/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,12 @@ def repr(cls, column, value):
def differences(cls, instance):
diff = {}
for attr, column in instance.__table__.columns.items():
if attr in ['currently_sending', 'last_send_time', 'unapproved_count', 'last_updated', 'last_synced']:
continue

new_val = getattr(instance, attr)
if new_val:
new_val = instance.coerce_column_data(column, new_val)
old_val = instance.orig_value_of(attr)
if old_val != new_val:
"""
Expand Down Expand Up @@ -200,10 +205,6 @@ def track(cls, action, instance):
data = cls.format(diff)
if len(diff) == 1 and 'badge_num' in diff and c.SHIFT_CUSTOM_BADGES:
action = c.AUTO_BADGE_SHIFT
if isinstance(instance, AutomatedEmail) and not diff.keys().isdisjoint(("currently_sending",
"last_send_time",
"unapproved_count")):
return
elif not data:
return
else:
Expand Down
38 changes: 25 additions & 13 deletions uber/payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ def send_authorizenet_txn(self, txn_type=c.AUTHCAPTURE, **params):
f"{error_code}: {error_msg}")

return "Transaction declined. Please ensure you are entering the correct " + \
"expiry date, card CVV/CVC, and ZIP Code™."
"expiration date, card CVV/CVC, and ZIP Code."
else:
if hasattr(response, 'transactionResponse') is True \
and hasattr(response.transactionResponse, 'errors') is True:
Expand Down Expand Up @@ -1284,17 +1284,22 @@ def handle_col_name(model, col_name, category):
if isinstance(cost, Iterable):
# A list of the same item at different prices, e.g., group badges
for price in cost:
if receipt:
receipt_items.append(ReceiptItem(receipt_id=receipt.id,
department=department,
category=category,
desc=desc,
amount=price,
count=cost[price],
revert_change=revert_change,
))
try:
price = int(price)
except ValueError:
log.exception(f"The price for {desc} ({price}) isn't a number!")
else:
receipt_items.append((desc, price, cost[price]))
if receipt:
receipt_items.append(ReceiptItem(receipt_id=receipt.id,
department=department,
category=category,
desc=desc,
amount=price,
count=cost[price],
revert_change=revert_change,
))
else:
receipt_items.append((desc, price, cost[price]))
elif receipt:
receipt_items.append(ReceiptItem(receipt_id=receipt.id,
department=department,
Expand Down Expand Up @@ -1389,7 +1394,7 @@ def process_receipt_change(cls, model, col_name, new_model, receipt=None, count=

@classmethod
def auto_update_receipt(self, model, receipt, params):
from uber.models import Group, ArtShowApplication, Session
from uber.models import Attendee, Group, ArtShowApplication, Session
if not receipt:
return []

Expand Down Expand Up @@ -1468,7 +1473,11 @@ def auto_update_receipt(self, model, receipt, params):
setattr(new_model, 'promo_code', None)
with Session() as session:
session.add_promo_code_to_attendee(new_model, val)
changed_params.append(key)
items = self.process_receipt_change(model, key, new_model, receipt)
if items:
for receipt_item in items:
if receipt_item.amount != 0:
receipt_items += [receipt_item]

if isinstance(model, Group):
# "badges" is a property and not a column, so we have to include it explicitly
Expand All @@ -1477,6 +1486,9 @@ def auto_update_receipt(self, model, receipt, params):
setattr(new_model, 'badges_update', int(maybe_badges_update))
changed_params.append('badges')

if isinstance(model, Attendee) and (model.qualifies_for_discounts != new_model.qualifies_for_discounts):
changed_params.append('birthdate')

for param in changed_params:
items = self.process_receipt_change(model, param, new_model, receipt)
if items:
Expand Down
18 changes: 9 additions & 9 deletions uber/receipt_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ def age_discount_credit(attendee, new_attendee=None):
if not old_cost:
return

if abs(attendee.age_discount) > old_cost:
if abs(attendee.age_discount * 100) > old_cost:
diff = old_cost * -1
else:
diff = attendee.age_discount * 100
Expand All @@ -389,7 +389,7 @@ def age_discount_credit(attendee, new_attendee=None):
if old_credit and new_credit:
return ("Update Age Discount", (new_credit - old_credit) * 100, c.BADGE_DISCOUNT)
elif old_credit:
return ("Remove Age Discount", old_credit * 100, c.BADGE_DISCOUNT)
return ("Remove Age Discount", old_credit * 100 * -1, c.BADGE_DISCOUNT)
elif new_credit:
return ("Add Age Discount", new_credit * 100, c.BADGE_DISCOUNT)

Expand All @@ -407,23 +407,23 @@ def promo_code_credit(attendee, new_attendee=None):
return ("Badge Comp (Promo Code)", discount * 100 * -1, c.ITEM_COMP)
else:
return ("Promo Code Discount", discount * 100 * -1, c.BADGE_DISCOUNT)

old_cost = attendee.badge_cost_with_promo_code * 100
new_cost = new_attendee.badge_cost_with_promo_code * 100

if old_cost == new_cost:
return

if attendee.promo_code and new_attendee.promo_code:
return ("Update Promo Code", new_cost - old_cost * 100 * -1, c.BADGE_DISCOUNT)
return ("Update Promo Code", new_cost - old_cost, c.BADGE_DISCOUNT)
elif attendee.promo_code:
if not old_cost:
return ("Remove Badge Comp (Promo Code)", new_cost - old_cost * 100 * -1, c.ITEM_COMP)
return ("Remove Promo Code", new_cost - old_cost * 100 * -1, c.BADGE_DISCOUNT)
return ("Remove Badge Comp (Promo Code)", new_cost, c.ITEM_COMP)
return ("Remove Promo Code", new_cost - old_cost, c.BADGE_DISCOUNT)
elif new_attendee.promo_code:
if not new_cost:
return ("Add Badge Comp (Promo Code)", new_cost - old_cost * 100 * -1, c.ITEM_COMP)
return ("Add Promo Code", new_cost - old_cost * 100 * -1, c.BADGE_DISCOUNT)
return ("Add Badge Comp (Promo Code)", old_cost * -1, c.ITEM_COMP)
return ("Add Promo Code", new_cost - old_cost, c.BADGE_DISCOUNT)


@receipt_calculation.Attendee
Expand Down
Loading

0 comments on commit acdffea

Please sign in to comment.