from __future__ import annotations from app.models import ChatSession, Tenant from app.tools import actions_tool from app.tools.registry import ToolContext OID = "gid://shopify/Order/1" class FakeShopify: """Dispatches by query keyword to canned GraphQL payloads.""" def __init__(self, *, returnable=True, cancel_error=None): self.returnable = returnable self.cancel_error = cancel_error self.calls = [] async def execute(self, query, variables=None): self.calls.append((query, variables)) if "OrderById" in query: return {"order": {"id": OID, "name": "#1001", "lineItems": {"edges": [ {"node": {"title": "X", "quantity": 2, "variant": {"id": "gid://shopify/ProductVariant/99"}}} ]}}} if "orderCancel" in query: errs = [{"message": self.cancel_error}] if self.cancel_error else [] return {"orderCancel": {"job": {"id": "j1"}, "orderCancelUserErrors": errs}} if "orderUpdate" in query: return {"orderUpdate": {"order": {"id": OID}, "userErrors": []}} if "Returnable" in query: edges = ( [{"node": {"returnableFulfillmentLineItems": {"edges": [ {"node": {"fulfillmentLineItem": {"id": "gid://shopify/FulfillmentLineItem/7"}, "quantity": 1}} ]}}}] if self.returnable else [] ) return {"returnableFulfillments": {"edges": edges}} if "returnCreate" in query: return {"returnCreate": {"return": {"id": "r1", "status": "OPEN"}, "userErrors": []}} return {} async def _verified_ctx(db, *, user_message="sí, confirmo", **kw): t = Tenant(slug="t", **kw) db.add(t) await db.flush() s = ChatSession(shop="x", tenant_id=t.id, verified=True, verified_order_id=OID) db.add(s) await db.flush() return ToolContext( db=db, session=s, tenant_id=t.id, shop="shop.myshopify.com", shopify=FakeShopify(), allow_cancel=kw.get("allow_cancel", False), allow_address_change=kw.get("allow_address_change", False), allow_returns=kw.get("allow_returns", False), user_message=user_message, # customer's latest message (the confirm gate reads it) ) async def test_reorder_builds_cart_link(db_session): ctx = await _verified_ctx(db_session) out = await actions_tool.reorder({}, ctx) assert out["status"] == "ok" assert out["checkout_url"] == "https://shop.myshopify.com/cart/99:2" async def test_action_requires_verified_order(db_session): t = Tenant(slug="t", allow_cancel=True) db_session.add(t) await db_session.flush() s = ChatSession(shop="x", tenant_id=t.id, verified=False) # not verified db_session.add(s) await db_session.flush() ctx = ToolContext(db=db_session, session=s, tenant_id=t.id, shop="s", shopify=FakeShopify(), allow_cancel=True) out = await actions_tool.cancel_order({}, ctx) assert out["status"] == "need_verification" async def test_cancel_blocked_when_toggle_off(db_session): ctx = await _verified_ctx(db_session, allow_cancel=False) out = await actions_tool.cancel_order({}, ctx) assert out["status"] == "not_allowed" async def test_cancel_succeeds_when_allowed(db_session): ctx = await _verified_ctx(db_session, allow_cancel=True) out = await actions_tool.cancel_order({"reason": "ya no lo quiero"}, ctx) assert out["status"] == "cancelled" async def test_change_address_succeeds_when_allowed(db_session): ctx = await _verified_ctx(db_session, allow_address_change=True) out = await actions_tool.change_address( {"address1": "Calle 1", "city": "Madrid", "zip": "28013", "country": "es"}, ctx ) assert out["status"] == "updated" async def test_start_return_succeeds_when_allowed(db_session): ctx = await _verified_ctx(db_session, allow_returns=True) out = await actions_tool.start_return({"reason": "llegó defectuoso"}, ctx) assert out["status"] == "return_requested" # --- confirmation gate: irreversible actions need an explicit "sí" ----------- async def test_return_blocked_without_confirmation(db_session): # The real incident: user typed "INCORRECTO" (meaning "no"); it must NOT fire. ctx = await _verified_ctx(db_session, allow_returns=True, user_message="INCORRECTO") out = await actions_tool.start_return({"reason": "INCORRECTO"}, ctx) assert out["status"] == "needs_confirmation" assert not ctx.shopify.calls # never reached Shopify async def test_cancel_blocked_without_confirmation(db_session): ctx = await _verified_ctx(db_session, allow_cancel=True, user_message="quiero cancelar") out = await actions_tool.cancel_order({}, ctx) assert out["status"] == "needs_confirmation" assert not ctx.shopify.calls async def test_change_address_blocked_without_confirmation(db_session): ctx = await _verified_ctx(db_session, allow_address_change=True, user_message="no") out = await actions_tool.change_address({"address1": "C/1", "city": "Madrid", "zip": "28013"}, ctx) assert out["status"] == "needs_confirmation" assert not ctx.shopify.calls async def test_cancel_proceeds_after_yes(db_session): ctx = await _verified_ctx(db_session, allow_cancel=True, user_message="sí, cancélalo") out = await actions_tool.cancel_order({}, ctx) assert out["status"] == "cancelled"