feat: update Printful client to v2 mockup-tasks API format
The v2 API uses products array with catalog source, nested layers in
placements, and GET /mockup-tasks/{id} for polling. Also removes
hardcoded domain in favor of PUBLIC_URL setting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a794f4732a
commit
e5317dab27
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class Settings(BaseSettings):
|
|||
|
||||
# App
|
||||
app_name: str = "rSwag"
|
||||
public_url: str = "https://rswag.online"
|
||||
debug: bool = False
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
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.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
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.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
|
|
|||
Loading…
Reference in New Issue