def test_amount_to_charge_just_fees_true(): opp = Opportunity() opp.amount = 10 opp.agreed_to_pay_fees = True actual = amount_to_charge(opp) expected = Decimal("10.53") assert actual == expected
def test_amount_to_charge_cents_just_fees_false(): opp = Opportunity() opp.amount = 10.50 opp.agreed_to_pay_fees = False actual = amount_to_charge(opp) expected = Decimal("10.50") assert actual == expected
def charge_cards(): lock = Lock(key="charge-cards-lock") lock.acquire() log = Log() log.it("---Starting batch card job...") three_days_ago = (datetime.now(tz=zone) - timedelta(days=14)).strftime("%Y-%m-%d") today = datetime.now(tz=zone).strftime("%Y-%m-%d") opportunities = Opportunity.list(begin=three_days_ago, end=today) log.it("---Processing charges...") log.it(f"Found {len(opportunities)} opportunities available to process.") for opportunity in opportunities: if not opportunity.stripe_customer_id: continue amount = amount_to_charge(opportunity) log.it( f"---- Charging ${amount} to {opportunity.stripe_customer_id} ({opportunity.name})" ) try: charge(opportunity) except ChargeException as e: logging.info("Batch charge error") e.send_slack_notification() log.send() lock.release()
def update_ach_charges(): lock = Lock(key='update-ach-charges-lock') lock.acquire() log = Log() log.it('---Starting batch ach job...') log.it('---Checking for status changes on ACH charges...') three_days_ago = (datetime.now(tz=zone) - timedelta(days=3)).strftime("%Y-%m-%d") today = datetime.now(tz=zone).strftime("%Y-%m-%d") opportunities = Opportunity.list(begin=three_days_ago, end=today, stage_name="ACH Pending") for opportunity in opportunities: if not opportunity.stripe_customer_id: continue amount = amount_to_charge(opportunity) log.it( f"---- ACH Charging ${amount} to {opportunity.stripe_customer_id} ({opportunity.name})" ) try: charge(opportunity) except ChargeException as e: logging.info("ACH batch charge error") e.send_slack_notification() log.send() lock.release()
def charge_cards(): lock = Lock(key="charge-cards-lock") lock.acquire() log = Log() log.it("---Starting batch job...") three_days_ago = (datetime.now(tz=zone) - timedelta(days=3)).strftime("%Y-%m-%d") today = datetime.now(tz=zone).strftime("%Y-%m-%d") opportunities = Opportunity.list(begin=three_days_ago, end=today) log.it("---Processing charges...") log.it(f"Found {len(opportunities)} opportunities available to process.") for opportunity in opportunities: if not opportunity.stripe_customer: continue amount = amount_to_charge(opportunity) log.it( f"---- Charging ${amount} to {opportunity.stripe_customer} ({opportunity.name})" ) charge(opportunity) log.send() lock.release()
def charge_cards(): lock = Lock(key="charge-cards-lock") lock.acquire() log = Log() log.it("---Starting batch job...") three_days_ago = (datetime.now(tz=zone) - timedelta(days=14)).strftime("%Y-%m-%d") today = datetime.now(tz=zone).strftime("%Y-%m-%d") opportunities = Opportunity.list(begin=three_days_ago, end=today) log.it("---Processing charges...") processing_msg = f"Found {len(opportunities)} opportunities available to process." log.it(processing_msg) send_slack_message( { "channel": "#stripe", "text": processing_msg, "icon_emoji": ":moneybag:", } ) for opportunity in opportunities: if not opportunity.stripe_customer: continue amount = amount_to_charge(opportunity) try: entry_name = opportunity.name # replaces non-ascii characters with "?" - See PR #851 encoded_name = entry_name.encode("ascii", "replace") decoded_name = encoded_name.decode("ascii") log.it( f"---- Charging ${amount} to {opportunity.stripe_customer} ({decoded_name})" ) except: log.it( f"---- Charging ${amount} to {opportunity.stripe_customer} ({opportunity.name})" ) logging.warn(f"Could not encode {opportunity.name}") try: charge(opportunity) except ChargeException as e: logging.info("Batch charge error") e.send_slack_notification() except QuarantinedException: logging.info( "Failed to charge because Opportunity %s is quarantined", opportunity ) log.send() lock.release()
def customer_source_updated(event): card_details = dict() # TODO update this with Opportunity fields when npsp is merged # we update all of these fields if any of them have changed because # we don't have these fields already populated; after some time that won't be # important if any([ "last4" in event["data"]["previous_attributes"], "brand" in event["data"]["previous_attributes"], "exp_year" in event["data"]["previous_attributes"], ]): year = event["data"]["object"]["exp_year"] month = event["data"]["object"]["exp_month"] day = calendar.monthrange(year, month)[1] expiration = f"{year}-{month:02d}-{day:02d}" card_details["Stripe_Card_Expiration__c"] = expiration card_details["Stripe_Card_Brand__c"] = event["data"]["object"]["brand"] card_details["Stripe_Card_Last_4__c"] = event["data"]["object"][ "last4"] else: logging.info("Event not relevant; discarding.") return opps = Opportunity.list( stage_name="Pledged", stripe_customer_id=event["data"]["object"]["customer"]) if not opps: return response = Opportunity.update_card(opps, card_details) logging.info(response) logging.info("card details updated")
def opportunity_from_eb_event( contact: Contact, attendee: dict, campaign: Campaign, org: Organization, event: dict, ticket_class: dict, ) -> Opportunity: ticket_category = ticket_class["category"] event_name = event["name"]["text"] attendee_name = f"{attendee['profile']['first_name']} {attendee['profile']['last_name']}" opportunity_name = f"{attendee_name} - {event_name}"[: 80] # limited to 80 chars ticket_class_name = ticket_class["name"] record_type_name = org.type_map[ticket_category] gross_amount = attendee["costs"]["gross"]["value"] / 100 base_price = attendee["costs"]["base_price"]["value"] / 100 donor_selected_amount = gross_amount if ticket_class[ "include_fee"] else base_price stage_name = "Refunded" if attendee["refunded"] else "Closed Won" eventbrite_id = attendee["id"] close_date = datetime.datetime.strptime(attendee["created"], "%Y-%m-%dT%H:%M:%SZ").date() # if the account, contact, lead source, amount, and close date are the same either throw an exception or consider it the same opp return Opportunity( account_id=contact.account_id, amount=gross_amount, campaign_id=campaign.id_, close_date=close_date, contact_id_for_role=contact.id_, donor_selected_amount=donor_selected_amount, eventbrite_ticket_type=ticket_class_name, eventbrite_id=eventbrite_id, lead_source="Eventbrite", name=opportunity_name, net_amount=base_price, record_type_name=record_type_name, sfc=org.sfc, stage_name=stage_name, )
def process_refund(transaction, org, ppc): opportunity = Opportunity.get( sfc=org.sfc, paypal_transaction_id=transaction.reference_id) opportunity.stage_name = "Refunded" opportunity.save() # since this was refunded there's a decent chance the sub was canceled; so we'd close the RD if not opportunity.recurring_donation_id: # there's no recurring donation; skip return recurring_donation = RecurringDonation.get( sfc=org.sfc, id_=opportunity.recurring_donation_id) if recurring_donation.open_ended_status == "Closed": # it's already closed; skip return raw_subscription = ppc.get_subscription( subscription_id=recurring_donation.paypal_subscription_id) subscription = PaypalSubscription.from_dict(data=raw_subscription) if subscription == "CANCELLED": recurring_donation.open_ended_status = "Closed" recurring_donation.save() return
def opportunity_from_paypal_transaction(transaction: PaypalTransaction, contact: Contact, org: Organization) -> Opportunity: if transaction.subject: name = f"PayPal: {transaction.subject} ({transaction.email})" else: name = f"PayPal: {transaction.email}" opportunity = Opportunity( account_id=contact.account_id, amount=transaction.gross_amount, donor_selected_amount=transaction.gross_amount, net_amount=transaction.gross_amount - transaction.fee_amount, sfc=org.sfc, stage_name="Closed Won", paypal_account_id=transaction.account_id, lead_source="PayPal", name=name, paypal_transaction_id=transaction.id_, close_date=transaction.transaction_date, encouraged_by=transaction.note, org_property=org.paypal_property, ) return opportunity
def test__format_amount(): opp = Opportunity(sf_connection=sf) opp.amount = "1500.123" actual = opp.amount expected = "1500.12" assert actual == expected opp.amount = "1500" actual = opp.amount expected = "1500.00" assert actual == expected opp.amount = "1500.00" actual = opp.amount expected = "1500.00" assert actual == expected opp.amount = "1500.126" actual = opp.amount expected = "1500.13" assert actual == expected
def authorization_notification(payload): amzn_id = payload["AuthorizationNotification"]["AuthorizationDetails"][ "AmazonAuthorizationId"] # trim everything after the last dash - seems like there should be a more # straightforward way to do this match = re.search("^(.*)[-]", amzn_id) amzn_id = match.group(1) logging.info(amzn_id) client = AmazonPayClient( mws_access_key=MWS_ACCESS_KEY, mws_secret_key=MWS_SECRET_KEY, merchant_id=AMAZON_MERCHANT_ID, region="na", currency_code="USD", sandbox=AMAZON_SANDBOX, ) response = client.get_order_reference_details( amazon_order_reference_id=amzn_id) response = response.to_dict() logging.info(json.dumps(response, indent=4)) details = response["GetOrderReferenceDetailsResponse"][ "GetOrderReferenceDetailsResult"]["OrderReferenceDetails"] amount = details["OrderTotal"]["Amount"] logging.info(amount) name = HumanName(details["Buyer"]["Name"]) first_name = name.first last_name = name.last email = details["Buyer"]["Email"] zipcode = get_zip(details=details) description = details["SellerOrderAttributes"]["StoreName"] logging.info("----Getting contact....") contact = Contact.get_or_create(email=email, first_name=first_name, last_name=last_name, zipcode=zipcode) logging.info(contact) if contact.first_name == "Subscriber" and contact.last_name == "Subscriber": logging.info(f"Changing name of contact to {first_name} {last_name}") contact.first_name = first_name contact.last_name = last_name contact.save() if contact.first_name != first_name or contact.last_name != last_name: logging.info( f"Contact name doesn't match: {contact.first_name} {contact.last_name}" ) if zipcode and not contact.created and contact.mailing_postal_code != zipcode: contact.mailing_postal_code = zipcode contact.save() logging.info("----Adding opportunity...") opportunity = Opportunity(contact=contact, stage_name="Closed Won") opportunity.amount = amount opportunity.description = description opportunity.lead_source = "Amazon Alexa" opportunity.amazon_order_id = amzn_id opportunity.campaign_id = AMAZON_CAMPAIGN_ID opportunity.name = ( f"[Alexa] {contact.first_name} {contact.last_name} ({contact.email})") opportunity.save() logging.info(opportunity) notify_slack(contact=contact, opportunity=opportunity) if contact.duplicate_found: send_multiple_account_warning(contact)
def test_generate_stripe_description(): # if description is blank use type opp = Opportunity(sf_connection=sf) opp.type = "Recurring Donation" opp.description = "" actual = generate_stripe_description(opp) assert actual == "Texas Tribune Sustaining Membership" # strip leading "The " opp = Opportunity(sf_connection=sf) opp.description = "The Cuddly Kitty" actual = generate_stripe_description(opp) assert actual == "Cuddly Kitty" # description overrides type opp = Opportunity(sf_connection=sf) opp.type = "Recurring Donation" opp.description = "Cats in Hats Are Cute!" actual = generate_stripe_description(opp) assert actual == "Cats in Hats Are Cute!" # if we can't find anything else at least they'll know it's from us opp = Opportunity(sf_connection=sf) opp.type = "Something Bogus" opp.description = "" actual = generate_stripe_description(opp) assert actual == "Texas Tribune"
def test__format_opportunity(): opportunity = Opportunity(sf_connection=sf) opportunity.account_id = "0011700000BpR8PAAV" opportunity.amount = 9 opportunity.encouraged_by = "Because I love the Trib!" opportunity.name = "D C ([email protected])" opportunity.stripe_id = "cus_78MqJSBejMN9gn" opportunity.agreed_to_pay_fees = True opportunity.referral_id = "1234" opportunity.lead_source = "Stripe" opportunity.description = "The Texas Tribune Membership" opportunity.stripe_customer = "cus_78MqJSBejMN9gn" response = opportunity._format() expected = { "AccountId": "0011700000BpR8PAAV", "CampaignId": None, "Amount": "9.00", "CloseDate": today, "Encouraged_to_contribute_by__c": "Because I love the Trib!", "LeadSource": "Stripe", "Name": "D C ([email protected])", "RecordType": { "Name": "Membership" }, "StageName": "Pledged", "Stripe_Customer_ID__c": "cus_78MqJSBejMN9gn", "Referral_ID__c": "1234", "Description": "The Texas Tribune Membership", "Stripe_Agreed_to_pay_fees__c": True, "Type": "Single", "Stripe_Card__c": None, "Stripe_Transaction_ID__c": None, "npsp__Closed_Lost_Reason__c": None, "Stripe_Card_Brand__c": None, "Stripe_Card_Expiration__c": None, "Stripe_Card_Last_4__c": None, "Amazon_Order_Id__c": None, } assert response == expected
def add_opportunity(contact=None, form=None, customer=None, quarantine=False): """ This will add a single donation to Salesforce. """ logging.info("----Adding opportunity...") opportunity = Opportunity(contact=contact) opportunity.amount = form.get("amount", 0) opportunity.stripe_customer = customer["id"] opportunity.campaign_id = form["campaign_id"] opportunity.referral_id = form["referral_id"] opportunity.description = "Texas Tribune Membership" opportunity.agreed_to_pay_fees = form["pay_fees_value"] opportunity.encouraged_by = form["reason"] opportunity.lead_source = "Stripe" opportunity.quarantined = quarantine customer = stripe.Customer.retrieve(customer["id"]) card = customer.sources.retrieve(customer.sources.data[0].id) year = card.exp_year month = card.exp_month day = calendar.monthrange(year, month)[1] opportunity.stripe_card_expiration = f"{year}-{month:02d}-{day:02d}" opportunity.stripe_card_brand = card.brand opportunity.stripe_card_last_4 = card.last4 opportunity.save() return opportunity
def test__format_slack(): opportunity = Opportunity(sf_connection=sf) opportunity.account_id = "0011700000BpR8PAAV" opportunity.amount = 9 opportunity.encouraged_by = "Because I love the Trib!" opportunity.name = "D C ([email protected])" opportunity.stripe_id = "cus_78MqJSBejMN9gn" opportunity.agreed_to_pay_fees = True opportunity.referral_id = "1234" opportunity.lead_source = "Stripe" opportunity.description = "The Texas Tribune Membership" opportunity.stripe_customer = "cus_78MqJSBejMN9gn" rdo = RDO(sf_connection=sf) rdo.referral_id = "1234" rdo.encouraged_by = "Because I love the Trib!" rdo.lead_source = "Stripe" rdo.contact_id = "0031700000BHQzBAAX" rdo.installment_period = "yearly" rdo.stripe_customer = "cus_78MqJSBejMN9gn" rdo.amount = 100 rdo.name = "foo" rdo.installments = 3 rdo.open_ended_status = None rdo.description = "Texas Tribune Circle Membership" rdo.agreed_to_pay_fees = True rdo.type = "Giving Circle" contact = Contact(sf_connection=sf) contact.email = "*****@*****.**" contact.first_name = "D" contact.last_name = "C" contact.lead_source = "Stripe" contact.work_email = "*****@*****.**" account = Account(sf_connection=sf) account.name = "Acme Inc." account.website = "http://acme.com" account.shipping_street = "Street" account.shipping_city = "Austin" account.shipping_postalcode = "78701" account.shipping_state = "TX" account.record_type_name = "Household" actual = construct_slack_message(account=account, rdo=rdo, opportunity=None, contact=None) expected = "Acme Inc. pledged $100 [yearly] (Because I love the Trib!)" assert actual == expected actual = construct_slack_message(account=None, rdo=rdo, opportunity=None, contact=contact) expected = "D C pledged $100 [yearly] (Because I love the Trib!)" assert actual == expected actual = construct_slack_message(account=None, rdo=None, opportunity=opportunity, contact=contact) expected = "D C pledged $9 [one-time] (Because I love the Trib!)" assert actual == expected
def test__campaign_id_validation(): ID_15_VALID_CHARS = "111AAA222bbb333" ID_18_VALID_CHARS = "111AAA222bbb333ccc" ID_15_INVALID_CHARS = "1!1A;-+22bbb333" ID_18_INVALID_CHARS = "111AAA222bbb333#c;" ID_INCORRECT_LENGTH = "AAADDD" opp = Opportunity(sf_connection=sf) opp.campaign_id = ID_15_VALID_CHARS assert not opp.has_invalid_campaign_id_format() opp.campaign_id = ID_18_VALID_CHARS assert not opp.has_invalid_campaign_id_format() opp.campaign_id = ID_15_INVALID_CHARS assert opp.has_invalid_campaign_id_format() opp.campaign_id = ID_18_INVALID_CHARS assert opp.has_invalid_campaign_id_format() opp.campaign_id = ID_INCORRECT_LENGTH assert opp.has_invalid_campaign_id_format()
def test_slackify_all(): bad_actor_item1 = BadActorResponseItem( label="foo", value="bar", judgment=BadActorJudgmentType.suspect) bad_actor_item2 = BadActorResponseItem(label="foo", value="bar", judgment=BadActorJudgmentType.good) bad_actor_response = BadActorResponse( overall_judgment=BadActorJudgmentType.suspect, items=[bad_actor_item1, bad_actor_item2], ) opportunity = Opportunity(sf_connection=sf) opportunity.id = "baz" bad_actor = BadActor(bad_actor_request=None) bad_actor.bad_actor_api_response = bad_actor_response bad_actor.transaction = opportunity bad_actor.transaction_type = "Opportunity" expected = [ { "fields": [{ "text": "<quux/baz|Salesforce>", "type": "mrkdwn" }], "type": "section", }, { "fields": [ { "text": ":eyes: *foo*: bar", "type": "mrkdwn" }, { "text": ":white_check_mark: *foo*: bar", "type": "mrkdwn" }, ], "type": "section", }, { "block_id": "choices", "elements": [ { "action_id": "approve", "style": "primary", "text": { "emoji": True, "text": "Approve", "type": "plain_text" }, "type": "button", "value": "Opportunity:baz", }, { "action_id": "reject", "style": "danger", "text": { "emoji": True, "text": "Reject", "type": "plain_text" }, "type": "button", "value": "Opportunity:baz", }, ], "type": "actions", }, ] actual = bad_actor._slackify_all() assert actual == expected
def add_business_opportunity(account=None, form=None, customer=None): """ Adds a single business membership to Salesforce. """ year = datetime.now(tz=ZONE).strftime("%Y") opportunity = Opportunity(account=account) opportunity.record_type_name = "Business Membership" opportunity.name = f"{year} Business {account.name} One time" opportunity.amount = form.get("amount", 0) opportunity.stripe_customer = customer["id"] opportunity.campaign_id = form["campaign_id"] opportunity.referral_id = form["referral_id"] opportunity.description = "Texas Tribune Business Membership" opportunity.agreed_to_pay_fees = form["pay_fees_value"] opportunity.encouraged_by = form["reason"] opportunity.lead_source = "Stripe" opportunity.save() return opportunity
def test_net_amount_none(): opp = Opportunity(sf_connection=sf) opp.net_amount = None assert opp.net_amount == "0.00"