Files
westfarn 1ca5a757d9
Deploy Beta / unit-tests (push) Successful in 31s
Deploy Beta / docker (push) Failing after 34s
Deploy Beta / deploy-beta (push) Skipped
Ship Print Forge shop site (colors, photos, 3D viewer) (#2)
## Summary

- Rebrand the Django client template as Print Forge (`client_site` → `print_forge`) with shop + shipping enabled.
- Product listings support multiple photos, color variants (shared price/description/STL, per-color stock and photos), and a photo-first / 3D-second gallery.
- Public pages use 3D printer / printed-toy photography instead of leftover t-shirt mockups; beta CI deploys on `master`.

Closes #1
Infra: [server-infra#27](ai_ml_operations/server-infra#27) (easy deploy beta).

## Test plan

- [ ] Product page shows photo first, 3D model second; color swatches swap photos and stock
- [ ] Portal can upload multiple photos and per-color qty/images; STL stays shared
- [ ] Public home/about/gallery have no t-shirt mockups
- [ ] `manage.py test` passes
- [ ] After server-infra#27: beta deploy to `print-forge-preview.aimloperations.com`

Reviewed-on: #2
2026-09-06 18:40:42 -07:00

71 lines
2.3 KiB
Python

import json
import logging
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from pos_sync.models import POSConnection, SyncEvent
from pos_sync.services import POSSyncError, apply_inbound_sale
logger = logging.getLogger(__name__)
def _authorized(request) -> bool:
expected = (settings.POS_WEBHOOK_SECRET or "").strip()
if not expected:
conn = POSConnection.objects.filter(is_active=True).exclude(
webhook_secret=""
).first()
expected = (conn.webhook_secret if conn else "").strip()
if not expected:
return False
header = request.headers.get("Authorization", "")
token = ""
if header.startswith("Bearer "):
token = header[7:].strip()
token = token or request.GET.get("token") or ""
return token == expected
@login_required
def event_list(request):
events = SyncEvent.objects.select_related("connection")[:200]
connections = POSConnection.objects.all()
return render(
request,
"pos_sync/list.html",
{"events": events, "connections": connections},
)
@csrf_exempt
@require_http_methods(["POST"])
def inventory_webhook(request):
if not _authorized(request):
return HttpResponse("unauthorized", status=401)
try:
payload = json.loads(request.body.decode() or "{}")
except json.JSONDecodeError:
return HttpResponseBadRequest("invalid json")
sku = payload.get("sku") or payload.get("SKU") or ""
try:
quantity = int(payload.get("quantity") or payload.get("qty") or 0)
except (TypeError, ValueError):
return HttpResponseBadRequest("invalid quantity")
external_id = str(payload.get("external_id") or payload.get("id") or "")
try:
event = apply_inbound_sale(
sku=sku,
quantity=quantity,
external_id=external_id,
payload=payload,
)
except POSSyncError as exc:
logger.warning("POS inbound rejected: %s", exc)
return JsonResponse({"ok": False, "error": str(exc)}, status=400)
return JsonResponse({"ok": True, "id": str(event.pk), "status": event.status})