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']}", }, }
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}", }, }
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']}", } }
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})"
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}"], }
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}')"
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
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']}", } }
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
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}", }, }