diff --git a/.env.example b/.env.example index 8a9d03a..53c1b82 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ JWT_SECRET=generate_a_strong_secret_here # App CORS_ORIGINS=https://rswag.online +PUBLIC_URL=https://rswag.online # AI Design Generation GEMINI_API_KEY=xxx diff --git a/backend/app/api/designs.py b/backend/app/api/designs.py index 001045a..d81cddf 100644 --- a/backend/app/api/designs.py +++ b/backend/app/api/designs.py @@ -152,22 +152,30 @@ async def _get_printful_mockup(slug: str, product) -> bytes | None: variant_ids = [variants[0]["id"]] # Public image URL for Printful to download - image_url = f"https://fungiswag.jeffemmett.com/api/designs/{slug}/image" + image_url = f"{settings.public_url}/api/designs/{slug}/image" # Generate mockup (blocks up to ~60s on first call) mockups = await printful.generate_mockup_and_wait( product_id=product_id, variant_ids=variant_ids, image_url=image_url, + placement="front", + technique="dtg", ) if not mockups: return None - # Find a mockup URL from the result + # v2 response: catalog_variant_mockups → each has mockup_url or + # placements[].mockup_url. Also check legacy "url" field. mockup_url = None for m in mockups: mockup_url = m.get("mockup_url") or m.get("url") + if not mockup_url and "placements" in m: + for p in m["placements"]: + mockup_url = p.get("mockup_url") or p.get("url") + if mockup_url: + break if mockup_url: break diff --git a/backend/app/config.py b/backend/app/config.py index cda632e..cb508f6 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -51,6 +51,7 @@ class Settings(BaseSettings): # App app_name: str = "rSwag" + public_url: str = "https://rswag.online" debug: bool = False @property diff --git a/backend/app/pod/printful_client.py b/backend/app/pod/printful_client.py index 5bb9d2d..f3f36c4 100644 --- a/backend/app/pod/printful_client.py +++ b/backend/app/pod/printful_client.py @@ -98,22 +98,37 @@ class PrintfulClient: product_id: int, variant_ids: list[int], image_url: str, - placement: str = "front_large", + placement: str = "front", + technique: str = "dtg", ) -> str: - """Start async mockup generation task. + """Start async mockup generation task (v2 format). Returns task_id to poll with get_mockup_task(). + + v2 payload uses products array with catalog source, and layers + inside placements instead of flat image_url. """ payload = { - "product_id": product_id, - "variant_ids": variant_ids, - "format": "png", - "placements": [ + "products": [ { - "placement": placement, - "image_url": image_url, + "source": "catalog", + "catalog_product_id": product_id, + "catalog_variant_ids": variant_ids, + "placements": [ + { + "placement": placement, + "technique": technique, + "layers": [ + { + "type": "file", + "url": image_url, + } + ], + } + ], } ], + "format": "png", } async with httpx.AsyncClient(timeout=30.0) as client: @@ -124,21 +139,20 @@ class PrintfulClient: ) resp.raise_for_status() data = resp.json().get("data", {}) - task_id = data.get("task_key") or data.get("id") or data.get("task_id") + task_id = data.get("id") or data.get("task_key") or data.get("task_id") logger.info(f"Printful mockup task created: {task_id}") return str(task_id) async def get_mockup_task(self, task_id: str) -> dict: - """Poll mockup task status. + """Poll mockup task status (v2 format). Returns dict with "status" (pending/completed/failed) and - "mockups" list when completed. + "catalog_variant_mockups" list when completed. """ async with httpx.AsyncClient(timeout=15.0) as client: resp = await client.get( - f"{BASE_URL}/mockup-tasks", + f"{BASE_URL}/mockup-tasks/{task_id}", headers=self._headers, - params={"task_key": task_id}, ) resp.raise_for_status() return resp.json().get("data", {}) @@ -148,7 +162,8 @@ class PrintfulClient: product_id: int, variant_ids: list[int], image_url: str, - placement: str = "front_large", + placement: str = "front", + technique: str = "dtg", max_polls: int = 20, poll_interval: float = 3.0, ) -> list[dict] | None: @@ -158,7 +173,7 @@ class PrintfulClient: or None on failure/timeout. """ task_id = await self.create_mockup_task( - product_id, variant_ids, image_url, placement + product_id, variant_ids, image_url, placement, technique ) for _ in range(max_polls): @@ -193,7 +208,7 @@ class PrintfulClient: - catalog_variant_id (int) - quantity (int) - image_url (str) — public URL to design - - placement (str, default "front_large") + - placement (str, default "front") recipient: dict with name, address1, city, state_code, country_code, zip, email (optional) """ @@ -208,7 +223,7 @@ class PrintfulClient: "quantity": item.get("quantity", 1), "placements": [ { - "placement": item.get("placement", "front_large"), + "placement": item.get("placement", "front"), "technique": "dtg", "layers": [ { diff --git a/backlog/tasks/task-5 - Add-real-Printful-mockup-API-integration.md b/backlog/tasks/task-5 - Add-real-Printful-mockup-API-integration.md index d540adb..b9cdd67 100644 --- a/backlog/tasks/task-5 - Add-real-Printful-mockup-API-integration.md +++ b/backlog/tasks/task-5 - Add-real-Printful-mockup-API-integration.md @@ -1,12 +1,13 @@ --- id: TASK-5 title: Add real Printful mockup API integration -status: To Do +status: In Progress assignee: [] created_date: '2026-02-18 19:51' +updated_date: '2026-02-21 20:54' labels: [] dependencies: [] -priority: low +priority: high --- ## Description @@ -14,3 +15,23 @@ priority: low Current upload page uses client-side Canvas compositing with simple template images. When Printful API token is configured, enhance with real Printful Mockup Generator API (POST /mockup-generator/create-task) for photorealistic product previews showing actual garment colors and fabric texture. + +## Implementation Notes + + +2026-02-21: Printful client code is DONE and deployed. Blocking issue: API token not scoped to store. + +What's done: +- backend/app/pod/printful_client.py created (catalog, mockups, orders) +- designs.py updated (Printful mockup path + Pillow fallback) +- order_service.py refactored (provider-aware routing: printful vs prodigi) +- Token stored at ~/.secrets/printful_api_token and in Netcup .env +- Deployed to fungiswag.jeffemmett.com (Pillow fallback working) + +Blocking: +- Token u5WU...R2d returns "This endpoint requires store_id" on mockup/order APIs +- Need to create a NEW token on developers.printful.com scoped to "Fungi Flows" store +- Select the store in the "Access" dropdown (not "Account (all stores)") + +Once new token is set, just update ~/.secrets/printful_api_token and Netcup .env, rebuild, done. +