예제 #1
0
def test_ingest_update_subscription_item(dbsession, stripe_subscription):
    """A subscription item can be updated."""
    data = stripe_subscription_data()
    new_price_id = fake_stripe_id("price", "monthly special")
    data["items"]["data"][0]["price"] = {
        "id": fake_stripe_id("price", "monthly special"),
        "object": "price",
        "created": unix_timestamp(datetime(2021, 10, 27, tzinfo=timezone.utc)),
        "product": FAKE_STRIPE_ID["Product"],
        "active": True,
        "currency": "usd",
        "recurring": {
            "interval": "month",
            "interval_count": 1,
        },
        "unit_amount": 499,
    }
    subscription, actions = ingest_stripe_subscription(dbsession, data)
    dbsession.commit()
    dbsession.refresh(subscription)

    assert len(subscription.subscription_items) == 1
    s_item = subscription.subscription_items[0]
    assert s_item.price.stripe_id == new_price_id
    assert actions == {
        "updated": {
            f"subscription_item:{data['items']['data'][0]['id']}",
        },
        "created": {
            f"price:{data['items']['data'][0]['price']['id']}",
        },
        "no_change": {
            f"subscription:{data['id']}",
        },
    }
예제 #2
0
def test_ingest_updated_invoice_lines(dbsession, stripe_invoice):
    """The Stripe Invoice lines are synced on update."""
    data = stripe_invoice_data()
    old_line_item_id = data["lines"]["data"][0]["id"]
    new_line_item_id = fake_stripe_id("il", "new line item")
    data["lines"]["data"][0] = {
        "id": new_line_item_id,
        "object": "line_item",
        "type": "subscription",
        "created": unix_timestamp(datetime(2021, 10, 28, tzinfo=timezone.utc)),
        "subscription": FAKE_STRIPE_ID["Subscription"],
        "subscription_item": FAKE_STRIPE_ID["Subscription Item"],
        "price": stripe_price_data(),
        "amount": 500,
        "currency": "usd",
    }
    invoice, actions = ingest_stripe_invoice(dbsession, data)
    dbsession.commit()
    assert len(invoice.line_items) == 1
    assert invoice.line_items[0].stripe_id == new_line_item_id
    assert actions == {
        "created": {
            f"line_item:{new_line_item_id}",
        },
        "no_change": {
            f"invoice:{data['id']}",
            f"price:{data['lines']['data'][0]['price']['id']}",
        },
        "deleted": {
            f"line_item:{old_line_item_id}",
        },
    }
예제 #3
0
def test_ingest_update_customer(dbsession, stripe_customer):
    """A Stripe Customer can be updated."""
    data = stripe_customer_data()
    # Change payment method
    new_source_id = fake_stripe_id("card", "new credit card")
    data["default_source"] = new_source_id
    data["invoice_settings"]["default_payment_method"] = None

    with StatementWatcher(dbsession.connection()) as watcher:
        customer, actions = ingest_stripe_customer(dbsession, data)
        dbsession.commit()
    assert watcher.count == 2
    stmt1 = watcher.statements[0][0]
    assert stmt1.startswith("SELECT stripe_customer."), stmt1
    assert stmt1.endswith(" FOR UPDATE"), stmt1
    stmt2 = watcher.statements[1][0]
    assert stmt2.startswith("UPDATE stripe_customer SET "), stmt2

    assert customer.default_source_id == new_source_id
    assert customer.invoice_settings_default_payment_method_id is None
    assert actions == {
        "updated": {
            f"customer:{data['id']}",
        }
    }
예제 #4
0
def test_ingest_unknown_stripe_object_raises(dbsession):
    """Ingesting an unknown type of Stripe object raises an exception."""
    re_id = fake_stripe_id("re", "refund")
    data = {"id": re_id, "object": "refund"}
    with pytest.raises(StripeIngestUnknownObjectError) as excinfo:
        ingest_stripe_object(dbsession, data)
    exception = excinfo.value
    assert str(exception) == f"Unknown Stripe object 'refund' with ID {re_id!r}."
    assert repr(exception) == f"StripeIngestUnknownObjectError('refund', {re_id!r})"
예제 #5
0
def test_api_post_deleted_new_customer(dbsession, client):
    """A new customer who starts out deleted is skipped."""
    stripe_id = fake_stripe_id("cus", "new_but_deleted")
    data = {"deleted": True, "id": stripe_id, "object": "customer"}
    with capture_logs() as caplog:
        resp = client.post("/stripe", json=data)
    assert resp.status_code == 200
    assert resp.json() == {"status": "OK"}
    assert len(caplog) == 1
    log = caplog[0]
    assert log["ingest_actions"] == {
        "skipped": [f"customer:{stripe_id}"],
    }
예제 #6
0
def test_ingest_new_customer_duplicate_fxa_id(
    dbsession, stripe_customer, example_contact
):
    """StripeIngestFxAIdConflict is raised when an existing customer has the same FxA ID."""
    data = stripe_customer_data()
    existing_id = data["id"]
    fxa_id = data["description"]
    data["id"] = fake_stripe_id("cust", "duplicate_fxa_id")
    with pytest.raises(StripeIngestFxAIdConflict) as excinfo:
        ingest_stripe_customer(dbsession, data)
    exception = excinfo.value
    assert (
        str(exception)
        == f"Existing StripeCustomer '{existing_id}' has FxA ID '{fxa_id}'."
    )
    assert repr(exception) == f"StripeIngestFxAIdConflict('{existing_id}', '{fxa_id}')"
예제 #7
0
def test_ingest_update_customer_duplicate_fxa_id(dbsession, stripe_customer):
    """StripeIngestFxAIdConflict is raised when updating to a different customer's FxA ID."""
    existing_customer_data = SAMPLE_STRIPE_DATA["Customer"].copy()
    existing_id = fake_stripe_id("cust", "duplicate_fxa_id")
    fxa_id = str(uuid4())
    existing_customer_data.update({"stripe_id": existing_id, "fxa_id": fxa_id})
    create_stripe_customer(
        dbsession, StripeCustomerCreateSchema(**existing_customer_data)
    )
    dbsession.commit()

    data = stripe_customer_data()
    data["description"] = fxa_id

    with pytest.raises(StripeIngestFxAIdConflict) as excinfo:
        ingest_stripe_customer(dbsession, data)

    exception = excinfo.value
    assert exception.stripe_id == existing_id
    assert exception.fxa_id == fxa_id
예제 #8
0
def test_ingest_non_recurring_price(dbsession):
    """A non-recurring price with no unit_amount can be ingested."""
    data = {
        "id": fake_stripe_id("price", "non-recurring"),
        "object": "price",
        "created": unix_timestamp(),
        "product": FAKE_STRIPE_ID["Product"],
        "active": True,
        "type": "one_time",
        "currency": "usd",
    }
    price, actions = ingest_stripe_price(dbsession, data)
    assert price.recurring_interval is None
    assert price.recurring_interval_count is None
    assert price.unit_amount is None
    assert price.get_email_id() is None
    assert actions == {
        "created": {
            f"price:{data['id']}",
        }
    }
예제 #9
0
def stripe_objects(dbsession, example_contact, maximal_contact):
    """
    Create two complete trees of Stripe objects.

    We don't use ForeignKeys for Stripe relations, because the object may come
    out of order for foreign key contraints. This helps check that the manually
    created relationships are correct.

    When following a relationship, look for this warning in the logs:
    SAWarning: Multiple rows returned with uselist=False for lazily-loaded attribute

    This suggests that SQLAlchemy is joining tables without a limiting WHERE clause,
    and the first item will be returned rather than the related item.
    """
    # Create prices / products
    # Both customers are subscribed to all four, 2 per subscription
    prices = []
    for price_idx in range(4):
        price_data = SAMPLE_STRIPE_DATA["Price"].copy()
        price_data["stripe_id"] = fake_stripe_id("price", f"price_{price_idx}")
        price_data["stripe_product_id"] = fake_stripe_id("prod", f"prod_{price_idx}")
        prices.append(
            create_stripe_price(dbsession, StripePriceCreateSchema(**price_data))
        )

    objs = []
    for contact_idx, contact in enumerate((example_contact, maximal_contact)):
        email_id = contact.email.email_id
        email = get_email(dbsession, email_id)
        obj = {
            "email_id": email_id,
            "contact": email,
            "customer": None,
            "subscription": [],
            "invoice": [],
        }
        objs.append(obj)

        # Create Customer data
        cus_data = SAMPLE_STRIPE_DATA["Customer"].copy()
        cus_data["stripe_id"] = fake_stripe_id("cus", f"cus_{contact_idx}")
        cus_data["fxa_id"] = contact.fxa.fxa_id
        obj["customer"] = create_stripe_customer(
            dbsession, StripeCustomerCreateSchema(**cus_data)
        )

        # Create Subscriptions / Invoices and related items
        for sub_inv_idx in range(2):
            sub_data = SAMPLE_STRIPE_DATA["Subscription"].copy()
            sub_data["stripe_id"] = fake_stripe_id(
                "sub", f"cus_{contact_idx}_sub_{sub_inv_idx}"
            )
            sub_data["stripe_customer_id"] = cus_data["stripe_id"]
            sub_obj = {
                "obj": create_stripe_subscription(
                    dbsession, StripeSubscriptionCreateSchema(**sub_data)
                ),
                "items": [],
            }
            obj["subscription"].append(sub_obj)

            inv_data = SAMPLE_STRIPE_DATA["Invoice"].copy()
            inv_data["stripe_id"] = fake_stripe_id(
                "sub", f"cus_{contact_idx}_inv_{sub_inv_idx}"
            )
            inv_data["stripe_customer_id"] = cus_data["stripe_id"]
            inv_obj = {
                "obj": create_stripe_invoice(
                    dbsession, StripeInvoiceCreateSchema(**inv_data)
                ),
                "line_items": [],
            }
            obj["invoice"].append(inv_obj)

            for item_idx in range(2):
                price = prices[sub_inv_idx * 2 + item_idx]

                si_data = SAMPLE_STRIPE_DATA["SubscriptionItem"].copy()
                si_data["stripe_id"] = fake_stripe_id(
                    "si", f"cus_{contact_idx}_sub_{sub_inv_idx}_si_{item_idx}"
                )
                si_data["stripe_price_id"] = price.stripe_id
                si_data["stripe_subscription_id"] = sub_data["stripe_id"]
                sub_obj["items"].append(
                    {
                        "obj": create_stripe_subscription_item(
                            dbsession, StripeSubscriptionItemCreateSchema(**si_data)
                        ),
                        "price": price,
                    }
                )

                li_data = SAMPLE_STRIPE_DATA["InvoiceLineItem"].copy()
                li_data["stripe_id"] = fake_stripe_id(
                    "il", f"cus_{contact_idx}_inv_{sub_inv_idx}_il_{item_idx}"
                )
                li_data["stripe_invoice_id"] = inv_data["stripe_id"]
                li_data["stripe_price_id"] = price.stripe_id
                li_data["stripe_subscription_id"] = sub_data["stripe_id"]
                li_data["stripe_subscription_item_id"] = si_data["stripe_id"]
                inv_obj["line_items"].append(
                    {
                        "obj": create_stripe_invoice_line_item(
                            dbsession, StripeInvoiceLineItemCreateSchema(**li_data)
                        ),
                        "price": price,
                    }
                )
    dbsession.commit()
    return objs
예제 #10
0
def test_ingest_update_subscription(dbsession, stripe_subscription):
    """An existing subscription is updated."""
    data = stripe_subscription_data()
    # Change to yearly
    current_period_end = stripe_subscription.current_period_end + timedelta(days=365)
    si_created = stripe_subscription.current_period_start + timedelta(days=15)
    data["current_period_end"] = unix_timestamp(current_period_end)
    old_sub_item_id = data["items"]["data"][0]["id"]
    new_sub_item_id = fake_stripe_id("si", "new subscription item id")
    new_price_id = fake_stripe_id("price", "yearly price")
    data["items"]["data"][0] = {
        "id": new_sub_item_id,
        "object": "subscription_item",
        "created": unix_timestamp(current_period_end),
        "subscription": data["id"],
        "price": {
            "id": new_price_id,
            "object": "price",
            "created": unix_timestamp(si_created),
            "product": FAKE_STRIPE_ID["Product"],
            "active": True,
            "currency": "usd",
            "recurring": {
                "interval": "year",
                "interval_count": 1,
            },
            "unit_amount": 4999,
        },
    }
    data["default_payment_method"] = fake_stripe_id("pm", "my new credit card")

    with StatementWatcher(dbsession.connection()) as watcher:
        subscription, actions = ingest_stripe_subscription(dbsession, data)
        dbsession.commit()
    assert watcher.count == 8
    stmt1, stmt2, stmt3, stmt4, stmt5, stmt6, stmt7, stmt8 = [
        pair[0] for pair in watcher.statements
    ]
    assert stmt1.startswith("SELECT stripe_subscription."), stmt1
    assert stmt1.endswith(" FOR UPDATE"), stmt1
    # Get all IDs
    assert stmt2.startswith("SELECT stripe_subscription_item.stripe_id "), stmt2
    assert stmt2.endswith(" FOR UPDATE"), stmt2
    # Load item 1
    # Can't eager load items with FOR UPDATE, need to query twice
    assert stmt3.startswith("SELECT stripe_price."), stmt3
    assert stmt3.endswith(" FOR UPDATE"), stmt3
    assert stmt4.startswith("SELECT stripe_subscription_item."), stmt4
    assert stmt4.endswith(" FOR UPDATE"), stmt4
    # Delete old item
    assert stmt5.startswith("DELETE FROM stripe_subscription_item "), stmt5
    # Insert order could be swapped
    insert = "INSERT INTO stripe_"
    update = "UPDATE stripe_subscription SET "
    assert stmt6.startswith(insert) or stmt6.startswith(update), stmt6
    assert stmt7.startswith(insert) or stmt7.startswith(update), stmt7
    assert stmt8.startswith(insert) or stmt8.startswith(update), stmt8

    assert subscription.current_period_end == current_period_end
    assert subscription.default_payment_method_id == data["default_payment_method"]

    assert len(subscription.subscription_items) == 1
    s_item = subscription.subscription_items[0]
    assert s_item.stripe_id == new_sub_item_id
    assert s_item.price.stripe_id == new_price_id

    assert actions == {
        "created": {
            f"subscription_item:{new_sub_item_id}",
            f"price:{new_price_id}",
        },
        "updated": {
            f"subscription:{data['id']}",
        },
        "deleted": {
            f"subscription_item:{old_sub_item_id}",
        },
    }