Files
handbook/docker/packages/shopify/tests/test_orders.py
T

933 lines
40 KiB
Python
Raw Normal View History

2026-06-24 12:44:34 -07:00
"""Tests for order management tools."""
import json
from typing import Any
import pytest
from pydantic import ValidationError
from shopify import state as shopify_state
from shopify.models import (
AppliedGiftCard,
CancelOrderArgs,
CreateOrderArgs,
GetOrderArgs,
ListOrdersArgs,
LooseCustomer,
LooseGiftCard,
LooseShippingMethod,
MoneyV2,
UpdateCartArgs,
UpdateOrderArgs,
)
from shopify.tools.cart import handle_update_cart
from shopify.tools.orders import (
handle_cancel_order,
handle_create_order,
handle_get_order,
handle_list_orders,
handle_update_order,
)
# Reusable test fixtures for required fields
VALID_CC = {"type": "credit_card", "card_number": "4111111111111111", "cvv": "123", "expiry": "12/26"}
VALID_PAYPAL = {"type": "paypal", "email": "buyer@example.com"}
VALID_GPAY = {"type": "google_pay", "email": "buyer@gmail.com"}
VALID_APPLEPAY = {"type": "apple_pay", "email": "buyer@icloud.com"}
VALID_ADDR = {
"firstName": "Alice",
"lastName": "Smith",
"address1": "123 Main St",
"city": "Springfield",
"countryCode": "US",
"zip": "62701",
}
def _order_args(**overrides: Any) -> CreateOrderArgs:
"""Build CreateOrderArgs with sensible defaults."""
return CreateOrderArgs.model_validate(
{
"cart_id": overrides.get("cart_id", "cart-1"),
"payment_method": overrides.get("payment_method", VALID_CC),
"shipping_address": overrides.get("shipping_address", VALID_ADDR),
"billing_address": overrides.get("billing_address", VALID_ADDR),
"shipping_method_id": overrides.get("shipping_method_id", "standard"),
"discount_code": overrides.get("discount_code"),
"email": overrides.get("email"),
"phone": overrides.get("phone"),
"note": overrides.get("note"),
"tags": overrides.get("tags"),
}
)
@pytest.fixture
def shopify_data(tmp_path):
"""Seed state with a product and a cart containing items."""
data_file = tmp_path / "shopify_data.json"
data_file.write_text(
json.dumps(
{
"products": {
"product-1": {
"id": "product-1",
"title": "Test Widget",
"handle": "test-widget",
"variants": [
{
"id": "variant-1",
"title": "Small",
"price": {"amount": "25.00", "currencyCode": "USD"},
"availableForSale": True,
"sku": "WIDGET-S",
},
],
}
},
"carts": {
"cart-1": {
"id": "cart-1",
"checkoutUrl": "https://shop.example.com/checkout/cart-1",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
"lines": [
{
"id": "line-1",
"quantity": 2,
"merchandise": {
"id": "variant-1",
"title": "Small",
"product": {"id": "product-1", "title": "Test Widget", "handle": "test-widget"},
"price": {"amount": "25.00", "currencyCode": "USD"},
"selectedOptions": [{"name": "Size", "value": "Small"}],
},
"cost": {
"amountPerQuantity": {"amount": "25.00", "currencyCode": "USD"},
"subtotalAmount": {"amount": "50.00", "currencyCode": "USD"},
"totalAmount": {"amount": "50.00", "currencyCode": "USD"},
},
"attributes": [],
"discountAllocations": [],
}
],
"cost": {
"subtotalAmount": {"amount": "50.00", "currencyCode": "USD"},
"totalAmount": {"amount": "50.00", "currencyCode": "USD"},
"checkoutChargeAmount": {"amount": "50.00", "currencyCode": "USD"},
},
"buyerIdentity": {"email": "alice@example.com", "phone": "+1234567890"},
"note": "Please gift wrap",
"totalQuantity": 2,
},
"cart-empty": {
"id": "cart-empty",
"checkoutUrl": "https://shop.example.com/checkout/cart-empty",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
"lines": [],
"cost": {
"subtotalAmount": {"amount": "0.00", "currencyCode": "USD"},
"totalAmount": {"amount": "0.00", "currencyCode": "USD"},
"checkoutChargeAmount": {"amount": "0.00", "currencyCode": "USD"},
},
"buyerIdentity": {},
"totalQuantity": 0,
},
},
"orders": {},
"customers": {},
"collections": {},
"reviews": {},
"returns": {},
"discount_codes": {
"dc-1": {
"id": "dc-1",
"code": "SAVE20",
"discountType": "PERCENTAGE",
"value": "20",
"minimumPurchase": None,
"usageLimit": None,
"usageCount": 0,
"productIds": None,
"active": True,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
},
"dc-2": {
"id": "dc-2",
"code": "FREESHIP",
"discountType": "FREE_SHIPPING",
"value": "0",
"minimumPurchase": None,
"usageLimit": None,
"usageCount": 0,
"productIds": None,
"active": True,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
},
"dc-3": {
"id": "dc-3",
"code": "WIDGET10",
"discountType": "FIXED_AMOUNT",
"value": "10.00",
"minimumPurchase": None,
"usageLimit": 1,
"usageCount": 0,
"productIds": ["product-1"],
"active": True,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
},
"dc-4": {
"id": "dc-4",
"code": "INACTIVE",
"discountType": "PERCENTAGE",
"value": "50",
"minimumPurchase": None,
"usageLimit": None,
"usageCount": 0,
"productIds": None,
"active": False,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
},
},
"gift_cards": {
"GIFT1234": {
"id": "gid://shopify/GiftCard/GIFT1234",
"code": "GIFT1234",
"balance": {"amount": "20.00", "currencyCode": "USD"},
"initialValue": {"amount": "20.00", "currencyCode": "USD"},
"active": True,
},
"EMPTYCARD": {
"id": "gid://shopify/GiftCard/EMPTYCARD",
"code": "EMPTYCARD",
"balance": {"amount": "0.00", "currencyCode": "USD"},
"active": True,
},
},
"shipping_methods": {
"standard": {
"id": "standard",
"title": "Standard Shipping",
"price": {"amount": "5.99", "currencyCode": "USD"},
"estimatedDays": "5-7 business days",
"active": True,
},
"express": {
"id": "express",
"title": "Express Shipping",
"price": {"amount": "14.99", "currencyCode": "USD"},
"estimatedDays": "1-2 business days",
"active": True,
},
},
"policies": [],
"counters": {
"cart_id": 1000,
"line_id": 1000,
"order_id": 2000,
"line_item_id": 3000,
"customer_id": 4000,
"collection_id": 5000,
"review_id": 6000,
"return_id": 7000,
"product_id": 8000,
"variant_id": 9000,
"discount_id": 10000,
"policy_id": 11000,
},
}
)
)
return data_file
@pytest.fixture(autouse=True)
def _patch_state(shopify_data, monkeypatch):
"""Reset state for each test."""
monkeypatch.setattr(shopify_state, "_STATE_FILE", shopify_data)
shopify_state._current_state = None
shopify_state._stores.clear()
shopify_state._active_store_id = "default"
shopify_state.load_state()
class TestCreateOrder:
def test_create_order_with_credit_card(self):
result = handle_create_order(_order_args())
assert result["userErrors"] == []
order = result["order"]
assert order["id"].startswith("gid://shopify/Order/")
assert order["financialStatus"] == "PAID"
assert order["fulfillmentStatus"] == "UNFULFILLED"
assert order["email"] == "alice@example.com"
assert order["paymentMethod"]["type"] == "credit_card"
assert order["paymentMethod"]["last4"] == "1111"
assert order["paymentMethod"]["brand"] == "visa"
assert order["shippingAddress"]["address1"] == "123 Main St"
assert order["billingAddress"]["city"] == "Springfield"
assert order["shippingMethod"]["id"] == "standard"
assert order["shippingPrice"]["amount"] == "5.99"
assert order["totalPrice"]["amount"] == "55.99" # 50.00 + 5.99 shipping
def test_create_order_with_paypal(self):
result = handle_create_order(_order_args(payment_method=VALID_PAYPAL))
order = result["order"]
assert order["paymentMethod"]["type"] == "paypal"
assert order["paymentMethod"]["email"] == "buyer@example.com"
assert order["financialStatus"] == "PAID"
def test_create_order_with_google_pay(self):
result = handle_create_order(_order_args(payment_method=VALID_GPAY))
assert result["order"]["paymentMethod"]["type"] == "google_pay"
def test_create_order_with_apple_pay(self):
result = handle_create_order(_order_args(payment_method=VALID_APPLEPAY))
assert result["order"]["paymentMethod"]["type"] == "apple_pay"
def test_mastercard_brand_detection(self):
mc = {"type": "credit_card", "card_number": "5500000000000004", "cvv": "123", "expiry": "12/26"}
result = handle_create_order(_order_args(payment_method=mc))
assert result["order"]["paymentMethod"]["brand"] == "mastercard"
def test_amex_brand_detection(self):
amex = {"type": "credit_card", "card_number": "371449635398431", "cvv": "1234", "expiry": "12/26"}
result = handle_create_order(_order_args(payment_method=amex))
assert result["order"]["paymentMethod"]["brand"] == "amex"
def test_card_number_with_spaces(self):
spaced = {"type": "credit_card", "card_number": "4111 1111 1111 1111", "cvv": "123", "expiry": "12/26"}
result = handle_create_order(_order_args(payment_method=spaced))
assert result["order"]["paymentMethod"]["last4"] == "1111"
def test_card_number_with_dashes(self):
dashed = {"type": "credit_card", "card_number": "4111-1111-1111-1111", "cvv": "123", "expiry": "12/26"}
result = handle_create_order(_order_args(payment_method=dashed))
assert result["order"]["paymentMethod"]["last4"] == "1111"
def test_invalid_card_number_too_short(self):
bad = {"type": "credit_card", "card_number": "411111", "cvv": "123", "expiry": "12/26"}
result = handle_create_order(_order_args(payment_method=bad))
assert result["order"] is None
assert any("card_number" in e["field"] for e in result["userErrors"])
def test_invalid_cvv(self):
bad = {"type": "credit_card", "card_number": "4111111111111111", "cvv": "12", "expiry": "12/26"}
result = handle_create_order(_order_args(payment_method=bad))
assert result["order"] is None
assert any("cvv" in e["field"] for e in result["userErrors"])
def test_invalid_expiry(self):
bad = {"type": "credit_card", "card_number": "4111111111111111", "cvv": "123", "expiry": "2026-12"}
result = handle_create_order(_order_args(payment_method=bad))
assert result["order"] is None
assert any("expiry" in e["field"] for e in result["userErrors"])
def test_invalid_payment_type(self):
with pytest.raises(ValidationError):
_order_args(payment_method={"type": "bitcoin"})
def test_paypal_missing_email(self):
with pytest.raises(ValidationError):
_order_args(payment_method={"type": "paypal"})
@pytest.mark.parametrize("email", ["@", "@@", " @", "a@", "buyer@example"])
def test_digital_wallet_invalid_email(self, email):
with pytest.raises(ValidationError):
_order_args(payment_method={"type": "paypal", "email": email})
def test_missing_shipping_address_field(self):
bad_addr = {"firstName": "Alice"} # missing address1, city, countryCode
result = handle_create_order(_order_args(shipping_address=bad_addr))
assert result["order"] is None
assert any("shipping_address" in e["field"] for e in result["userErrors"])
def test_missing_billing_address_field(self):
bad_addr = {"address1": "123 Main St"} # missing city, countryCode
result = handle_create_order(_order_args(billing_address=bad_addr))
assert result["order"] is None
assert any("billing_address" in e["field"] for e in result["userErrors"])
def test_create_order_empty_cart(self):
result = handle_create_order(_order_args(cart_id="cart-empty"))
assert result["order"] is None
assert "empty" in result["userErrors"][0]["message"]
def test_create_order_nonexistent_cart(self):
result = handle_create_order(_order_args(cart_id="nonexistent"))
assert result["order"] is None
def test_create_order_overrides_buyer_identity(self):
result = handle_create_order(_order_args(email="bob@example.com", phone="+9999999999"))
order = result["order"]
assert order["email"] == "bob@example.com"
assert order["phone"] == "+9999999999"
def test_create_order_with_tags(self):
result = handle_create_order(_order_args(tags=["vip", "rush"]))
assert result["order"]["tags"] == ["vip", "rush"]
def test_create_order_generates_line_item_ids(self):
result = handle_create_order(_order_args())
line = result["order"]["lineItems"][0]
assert line["id"].startswith("gid://shopify/OrderLineItem/")
class TestGetOrder:
def test_get_existing_order(self):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
result = handle_get_order(GetOrderArgs(order_id=order_id))
assert result["userErrors"] == []
assert result["order"]["id"] == order_id
def test_get_nonexistent_order(self):
result = handle_get_order(GetOrderArgs(order_id="nonexistent"))
assert result["order"] is None
class TestListOrders:
def test_list_empty(self):
result = handle_list_orders(ListOrdersArgs())
assert result["totalCount"] == 0
def test_list_with_orders(self):
handle_create_order(_order_args())
result = handle_list_orders(ListOrdersArgs())
assert result["totalCount"] == 1
def test_list_filter_by_financial_status(self):
handle_create_order(_order_args())
result = handle_list_orders(ListOrdersArgs(status="PAID"))
assert result["totalCount"] == 1
result = handle_list_orders(ListOrdersArgs(status="PENDING"))
assert result["totalCount"] == 0
def test_list_filter_by_fulfillment_status(self):
handle_create_order(_order_args())
result = handle_list_orders(ListOrdersArgs(status="UNFULFILLED"))
assert result["totalCount"] == 1
def test_list_pagination(self):
handle_create_order(_order_args())
handle_create_order(_order_args())
result = handle_list_orders(ListOrdersArgs(limit=1))
assert len(result["orders"]) == 1
assert result["totalCount"] == 2
assert result["pageInfo"]["hasNextPage"] is True
class TestUpdateOrder:
def test_update_financial_status_to_paid(self):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
shopify_state.get_state().orders[order_id].financialStatus = "PENDING"
result = handle_update_order(UpdateOrderArgs(order_id=order_id, financial_status="PAID"))
assert result["userErrors"] == []
assert result["order"]["financialStatus"] == "PAID"
@pytest.mark.parametrize("status", ["VOIDED", "REFUNDED", "PARTIALLY_REFUNDED"])
def test_update_financial_status_rejects_side_effect_statuses(self, status):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
result = handle_update_order(UpdateOrderArgs(order_id=order_id, financial_status=status))
assert result["order"] is None
assert result["userErrors"][0]["field"] == "financial_status"
assert shopify_state.get_state().orders[order_id].financialStatus == "PAID"
@pytest.mark.parametrize("status", ["VOIDED", "REFUNDED", "PARTIALLY_REFUNDED"])
def test_update_financial_status_rejects_leaving_side_effect_statuses(self, status):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
shopify_state.get_state().orders[order_id].financialStatus = status
result = handle_update_order(UpdateOrderArgs(order_id=order_id, financial_status="PAID"))
assert result["order"] is None
assert result["userErrors"][0]["field"] == "financial_status"
assert shopify_state.get_state().orders[order_id].financialStatus == status
def test_update_fulfillment_status(self):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
result = handle_update_order(UpdateOrderArgs(order_id=order_id, fulfillment_status="FULFILLED"))
assert result["order"]["fulfillmentStatus"] == "FULFILLED"
def test_update_invalid_status(self):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
with pytest.raises(ValidationError):
UpdateOrderArgs.model_validate({"order_id": order_id, "financial_status": "INVALID"})
def test_update_note_and_tags(self):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
result = handle_update_order(UpdateOrderArgs(order_id=order_id, note="Updated note", tags=["priority"]))
assert result["order"]["note"] == "Updated note"
assert result["order"]["tags"] == ["priority"]
def test_update_rejects_customer_email_reassignment(self):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
result = handle_update_order(UpdateOrderArgs(order_id=order_id, email="new@example.com"))
assert result["order"] is None
assert result["userErrors"][0]["field"] == "email"
assert shopify_state.get_state().orders[order_id].email == "alice@example.com"
def test_update_rejects_email_reassignment_before_any_mutation(self):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
order = shopify_state.get_state().orders[order_id]
original_updated_at = order.updatedAt
result = handle_update_order(
UpdateOrderArgs(
order_id=order_id,
financial_status="PAID",
fulfillment_status="FULFILLED",
note="Updated note",
tags=["urgent"],
email="new@example.com",
)
)
assert result["order"] is None
assert result["userErrors"][0]["field"] == "email"
assert order.financialStatus == "PAID"
assert order.fulfillmentStatus == "UNFULFILLED"
assert order.trackingNumber is None
assert order.trackingUrl is None
assert order.note == "Please gift wrap"
assert order.tags == []
assert order.email == "alice@example.com"
assert order.updatedAt == original_updated_at
def test_update_nonexistent_order(self):
result = handle_update_order(UpdateOrderArgs(order_id="nonexistent"))
assert result["order"] is None
class TestCancelOrder:
def test_cancel_order(self):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
result = handle_cancel_order(CancelOrderArgs(order_id=order_id))
assert result["userErrors"] == []
assert result["order"]["cancelledAt"] is not None
assert result["order"]["financialStatus"] == "REFUNDED"
def test_cancel_pending_order_voids_payment(self):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
shopify_state.get_state().orders[order_id].financialStatus = "PENDING"
result = handle_cancel_order(CancelOrderArgs(order_id=order_id))
assert result["userErrors"] == []
assert result["order"]["cancelledAt"] is not None
assert result["order"]["financialStatus"] == "VOIDED"
def test_cancel_reverses_checkout_side_effects(self):
state = shopify_state.get_state()
state.loyalty_program.enabled = True
state.customers["cust-1"] = LooseCustomer.model_validate(
{
"id": "cust-1",
"email": "alice@example.com",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
"ordersCount": 0,
"totalSpent": {"amount": "0.00", "currencyCode": "USD"},
"pointsBalance": 1000,
"lifetimePoints": 1000,
}
)
handle_update_cart(UpdateCartArgs(cart_id="cart-1", gift_card_codes=["GIFT1234"]))
create_result = handle_create_order(
CreateOrderArgs.model_validate(
{
"cart_id": "cart-1",
"payment_method": VALID_CC,
"shipping_address": VALID_ADDR,
"billing_address": VALID_ADDR,
"shipping_method_id": "standard",
"discount_code": "WIDGET10",
"email": "alice@example.com",
"redeem_points": 100,
}
)
)
order_id = create_result["order"]["id"]
customer = state.customers["cust-1"]
assert state.discount_codes["dc-3"].usageCount == 1
assert state.gift_cards["GIFT1234"].balance.amount == "0.00"
assert customer.ordersCount == 1
assert customer.totalSpent is not None
assert customer.totalSpent.amount == "24.99"
assert customer.pointsBalance == 939
assert customer.lifetimePoints == 1039
result = handle_cancel_order(CancelOrderArgs(order_id=order_id))
assert result["userErrors"] == []
assert result["order"]["sideEffectsReversedAt"] is not None
assert state.discount_codes["dc-3"].usageCount == 0
assert state.gift_cards["GIFT1234"].balance.amount == "20.00"
assert customer.ordersCount == 0
assert customer.totalSpent is not None
assert customer.totalSpent.amount == "0.00"
assert customer.pointsBalance == 1000
assert customer.lifetimePoints == 1000
def test_cancel_with_reason(self):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
result = handle_cancel_order(CancelOrderArgs(order_id=order_id, reason="Customer changed mind"))
assert "Customer changed mind" in result["order"]["note"]
def test_cancel_already_cancelled(self):
create_result = handle_create_order(_order_args())
order_id = create_result["order"]["id"]
handle_cancel_order(CancelOrderArgs(order_id=order_id))
result = handle_cancel_order(CancelOrderArgs(order_id=order_id))
assert result["order"] is None
assert "already cancelled" in result["userErrors"][0]["message"]
def test_cancel_nonexistent_order(self):
result = handle_cancel_order(CancelOrderArgs(order_id="nonexistent"))
assert result["order"] is None
class TestDiscountApplication:
def test_percentage_discount(self):
# SAVE20 = 20% off subtotal ($50) = $10 off → total = 50 - 10 + 5.99 = 45.99
result = handle_create_order(_order_args(discount_code="SAVE20"))
order = result["order"]
assert order["discount"]["code"] == "SAVE20"
assert order["discountAmount"]["amount"] == "10.00"
assert order["subtotalPrice"]["amount"] == "50.00"
assert order["shippingPrice"]["amount"] == "5.99"
assert order["totalPrice"]["amount"] == "45.99"
def test_free_shipping_discount(self):
# FREESHIP removes shipping cost → total = 50 + 0 = 50.00
result = handle_create_order(_order_args(discount_code="FREESHIP"))
order = result["order"]
assert order["shippingPrice"]["amount"] == "0.00"
assert order["discountAmount"]["amount"] == "5.99"
assert order["totalPrice"]["amount"] == "50.00"
def test_product_scoped_fixed_discount(self):
# WIDGET10 = $10 off product-1 items only → 50 - 10 + 5.99 = 45.99
result = handle_create_order(_order_args(discount_code="WIDGET10"))
order = result["order"]
assert order["discountAmount"]["amount"] == "10.00"
assert order["totalPrice"]["amount"] == "45.99"
def test_discount_increments_usage(self):
handle_create_order(_order_args(discount_code="SAVE20"))
from shopify.state import get_discount_by_code
dc = get_discount_by_code("SAVE20")
assert dc is not None
assert dc["usageCount"] == 1
def test_usage_limit_enforced(self):
# WIDGET10 has usageLimit=1
handle_create_order(_order_args(discount_code="WIDGET10"))
result = handle_create_order(_order_args(discount_code="WIDGET10"))
assert result["order"] is None
assert "usage limit" in result["userErrors"][0]["message"]
def test_inactive_discount_rejected(self):
state = shopify_state.get_state()
before_order_id = state.counters.order_id
before_line_item_id = state.counters.line_item_id
result = handle_create_order(_order_args(discount_code="INACTIVE"))
assert result["order"] is None
assert "not active" in result["userErrors"][0]["message"]
assert state.counters.order_id == before_order_id
assert state.counters.line_item_id == before_line_item_id
def test_nonexistent_discount_rejected(self):
result = handle_create_order(_order_args(discount_code="DOESNTEXIST"))
assert result["order"] is None
assert "not found" in result["userErrors"][0]["message"]
def test_no_discount_no_discount_info(self):
result = handle_create_order(_order_args())
assert result["order"]["discount"] is None
assert result["order"]["discountAmount"]["amount"] == "0.00"
def test_cart_discount_applies_when_checkout_has_no_explicit_discount(self):
handle_update_cart(UpdateCartArgs(cart_id="cart-1", discount_codes=["SAVE20"]))
result = handle_create_order(_order_args())
order = result["order"]
assert order["discount"]["code"] == "SAVE20"
assert order["discountAmount"]["amount"] == "10.00"
assert order["totalPrice"]["amount"] == "45.99"
def test_cart_marks_non_combinable_second_discount_not_applicable(self):
result = handle_update_cart(UpdateCartArgs(cart_id="cart-1", discount_codes=["SAVE20", "FREESHIP"]))
assert result["cart"]["discountCodes"][0]["applicable"] is True
assert result["cart"]["discountCodes"][1]["applicable"] is False
assert "cannot be combined" in result["userErrors"][0]["message"]
def test_cart_applies_multiple_combinable_discounts_at_checkout(self):
state = shopify_state.get_state()
state.discount_codes["dc-1"].combinesWith.shippingDiscounts = True
state.discount_codes["dc-2"].combinesWith.orderDiscounts = True
cart_result = handle_update_cart(UpdateCartArgs(cart_id="cart-1", discount_codes=["SAVE20", "FREESHIP"]))
result = handle_create_order(_order_args())
order = result["order"]
assert [code["applicable"] for code in cart_result["discountCodes"]] == [True, True]
assert order["discount"]["code"] == "SAVE20"
assert [discount["code"] for discount in order["discounts"]] == ["SAVE20", "FREESHIP"]
assert order["discountAmount"]["amount"] == "15.99"
assert order["shippingPrice"]["amount"] == "0.00"
assert order["totalPrice"]["amount"] == "40.00"
def test_update_cart_marks_valid_discount_applicable(self):
result = handle_update_cart(UpdateCartArgs(cart_id="cart-1", discount_codes=["SAVE20"]))
assert result["discountCodes"][0]["code"] == "SAVE20"
assert result["discountCodes"][0]["applicable"] is True
def test_update_cart_marks_nonexistent_discount_not_applicable(self):
result = handle_update_cart(UpdateCartArgs(cart_id="cart-1", discount_codes=["DOESNTEXIST"]))
assert result["cart"]["discountCodes"][0]["code"] == "DOESNTEXIST"
assert result["cart"]["discountCodes"][0]["applicable"] is False
assert "not found" in result["userErrors"][0]["message"]
def test_update_cart_marks_inactive_discount_not_applicable(self):
result = handle_update_cart(UpdateCartArgs(cart_id="cart-1", discount_codes=["INACTIVE"]))
assert result["cart"]["discountCodes"][0]["applicable"] is False
assert "not active" in result["userErrors"][0]["message"]
def test_update_cart_marks_minimum_purchase_discount_not_applicable(self):
discount = shopify_state.get_state().discount_codes["dc-1"]
discount.code = "MIN100"
discount.minimumPurchase = MoneyV2(amount="100.00", currencyCode="USD")
result = handle_update_cart(UpdateCartArgs(cart_id="cart-1", discount_codes=["MIN100"]))
assert result["cart"]["discountCodes"][0]["applicable"] is False
assert "Minimum purchase" in result["userErrors"][0]["message"]
def test_cart_delivery_options_and_gift_cards_are_stored(self):
result = handle_update_cart(
UpdateCartArgs.model_validate(
{
"cart_id": "cart-1",
"delivery_addresses_to_add": [
{
"address": {
"address1": "123 Main St",
"city": "Springfield",
"countryCode": "US",
}
}
],
"gift_card_codes": ["GIFT1234"],
}
)
)
cart = result
assert cart["buyerIdentity"]["deliveryAddressPreferences"][0]["address1"] == "123 Main St"
assert cart["deliveryGroups"][0]["id"].endswith("/delivery-group/1")
assert cart["deliveryGroups"][0]["deliveryOptions"][0]["handle"] == "standard"
assert cart["deliveryGroups"][0]["deliveryOptions"][0]["estimatedCost"]["amount"] == "5.99"
assert cart["deliveryGroups"][0]["deliveryOptions"][1]["handle"] == "express"
assert cart["deliveryGroups"][0]["deliveryOptions"][1]["estimatedCost"]["amount"] == "14.99"
assert cart["appliedGiftCards"][0]["code"] == "GIFT1234"
assert cart["appliedGiftCards"][0]["lastCharacters"] == "1234"
assert cart["appliedGiftCards"][0]["amountUsed"]["amount"] == "20.00"
assert cart["appliedGiftCards"][0]["balance"]["amount"] == "0.00"
assert cart["cost"]["checkoutChargeAmount"]["amount"] == "30.00"
selected_cart = handle_update_cart(
UpdateCartArgs.model_validate(
{
"cart_id": "cart-1",
"selected_delivery_options": [
{
"deliveryGroupId": "cart-1/delivery-group/1",
"deliveryOptionHandle": "express",
}
],
}
)
)
assert selected_cart["deliveryGroups"][0]["selectedDeliveryOption"]["handle"] == "express"
def test_cart_delivery_options_use_configured_active_shipping_methods(self):
state = shopify_state.get_state()
state.shipping_methods["standard"].active = False
state.shipping_methods["local-bike"] = LooseShippingMethod.model_validate(
{
"id": "local-bike",
"title": "Local Bike Courier",
"price": {"amount": "3.50", "currencyCode": "USD"},
"estimatedDays": "Same day",
"active": True,
}
)
result = handle_update_cart(
UpdateCartArgs.model_validate(
{
"cart_id": "cart-1",
"delivery_addresses_to_add": [
{
"address": {
"address1": "123 Main St",
"city": "Springfield",
"countryCode": "US",
}
}
],
}
)
)
options = result["deliveryGroups"][0]["deliveryOptions"]
assert [option["handle"] for option in options] == ["express", "local-bike"]
assert options[0]["estimatedCost"]["amount"] == "14.99"
assert options[1]["estimatedCost"]["amount"] == "3.50"
def test_create_order_uses_selected_cart_delivery_option_when_shipping_omitted(self):
handle_update_cart(
UpdateCartArgs.model_validate(
{
"cart_id": "cart-1",
"delivery_addresses_to_add": [
{
"address": {
"address1": "123 Main St",
"city": "Springfield",
"countryCode": "US",
}
}
],
}
)
)
handle_update_cart(
UpdateCartArgs.model_validate(
{
"cart_id": "cart-1",
"selected_delivery_options": [
{
"deliveryGroupId": "cart-1/delivery-group/1",
"deliveryOptionHandle": "express",
}
],
}
)
)
result = handle_create_order(
CreateOrderArgs.model_validate(
{
"cart_id": "cart-1",
"payment_method": VALID_CC,
"shipping_address": VALID_ADDR,
"billing_address": VALID_ADDR,
}
)
)
assert result["userErrors"] == []
assert result["order"]["shippingMethod"]["id"] == "express"
assert result["order"]["shippingPrice"]["amount"] == "14.99"
def test_cart_rejects_unknown_and_empty_gift_cards(self):
result = handle_update_cart(UpdateCartArgs(cart_id="cart-1", gift_card_codes=["NOPE", "EMPTYCARD"]))
assert result["cart"]["appliedGiftCards"] == []
assert len(result["userErrors"]) == 2
assert "not found" in result["userErrors"][0]["message"]
assert "no remaining balance" in result["userErrors"][1]["message"]
def test_create_order_applies_gift_card_balance(self):
handle_update_cart(UpdateCartArgs(cart_id="cart-1", gift_card_codes=["GIFT1234"]))
result = handle_create_order(_order_args())
order = result["order"]
assert order["giftCardAmount"]["amount"] == "20.00"
assert order["appliedGiftCards"][0]["code"] == "GIFT1234"
assert order["appliedGiftCards"][0]["amountUsed"]["amount"] == "20.00"
assert order["totalPrice"]["amount"] == "35.99"
assert shopify_state.get_state().gift_cards["GIFT1234"].balance.amount == "0.00"
def test_create_order_applies_gift_card_code_with_slash(self):
state = shopify_state.get_state()
state.gift_cards["GIFT/2024"] = LooseGiftCard.model_validate(
{
"id": "gid://shopify/GiftCard/GIFT/2024",
"code": "GIFT/2024",
"balance": {"amount": "10.00", "currencyCode": "USD"},
"initialValue": {"amount": "10.00", "currencyCode": "USD"},
"active": True,
}
)
handle_update_cart(UpdateCartArgs(cart_id="cart-1", gift_card_codes=["GIFT/2024"]))
result = handle_create_order(_order_args())
assert result["userErrors"] == []
assert result["order"]["appliedGiftCards"][0]["code"] == "GIFT/2024"
assert result["order"]["giftCardAmount"]["amount"] == "10.00"
assert state.gift_cards["GIFT/2024"].balance.amount == "0.00"
def test_create_order_validates_all_gift_cards_before_mutating(self):
state = shopify_state.get_state()
handle_update_cart(UpdateCartArgs(cart_id="cart-1", gift_card_codes=["GIFT1234"]))
state.gift_cards["STALE"] = LooseGiftCard.model_validate(
{
"id": "gid://shopify/GiftCard/STALE",
"code": "STALE",
"balance": {"amount": "10.00", "currencyCode": "USD"},
"active": False,
}
)
state.carts["cart-1"].appliedGiftCards.append(
AppliedGiftCard.model_validate(
{
"id": "gid://shopify/AppliedGiftCard/STALE",
"code": "STALE",
"lastCharacters": "TALE",
"amountUsed": {"amount": "10.00", "currencyCode": "USD"},
"balance": {"amount": "0.00", "currencyCode": "USD"},
"presentmentAmountUsed": {"amount": "10.00", "currencyCode": "USD"},
}
)
)
result = handle_create_order(_order_args(discount_code="WIDGET10"))
assert result["order"] is None
assert "not active" in result["userErrors"][0]["message"]
assert state.gift_cards["GIFT1234"].balance.amount == "20.00"
assert state.discount_codes["dc-3"].usageCount == 0
assert state.counters.order_id == 2000
assert state.counters.line_item_id == 3000
def test_order_without_discount_code(self):
# Verify orders still work fine without any discount
result = handle_create_order(_order_args())
assert result["userErrors"] == []
assert result["order"]["totalPrice"]["amount"] == "55.99" # 50 + 5.99