Template
Add shop, POS sync, event ticketing, and shipping catalog apps (#6)
## Summary - Closes #5 - Optional `shop`, `pos_sync`, `events`, and `shipping` apps gated by `FEATURE_*` flags - Deps match the catalog: shop/events need email + Stripe; POS/shipping need shop - Portal inventory, POS webhooks, capacity tickets, EasyPost/Pirate Ship shipping ## Test plan - [ ] `manage.py test` (152 passed locally) - [ ] Shop cart + paid order decrements stocked inventory - [ ] POS inbound webhook decrements SKU; paid order queues outbound reserve - [ ] Event capacity blocks overbook; paid order emails ticket codes - [ ] Shipping stub label + Pirate Ship CSV of unshipped paid orders - [ ] `validate-env.sh` rejects shop without email/payments, POS/shipping without shop Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
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})
|
||||
Reference in New Issue
Block a user