933 lines
40 KiB
Python
933 lines
40 KiB
Python
|
|
"""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
|