Example #1
0
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
Example #2
0
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
Example #3
0
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()
Example #4
0
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()
Example #5
0
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()
Example #6
0
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()
Example #7
0
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")
Example #8
0
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
Example #11
0
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
Example #12
0
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)
Example #13
0
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"
Example #14
0
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
Example #15
0
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
Example #16
0
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
Example #17
0
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()
Example #18
0
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
Example #19
0
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
Example #20
0
def test_net_amount_none():
    opp = Opportunity(sf_connection=sf)
    opp.net_amount = None
    assert opp.net_amount == "0.00"