Stand up Print Forge as a 3D-printed toy shop with color variants and printer photography.

Replaces the client_site template branding, adds shop/shipping, and points beta CI at master for easy deploy.

Closes #1
This commit is contained in:
2026-09-06 08:28:04 -05:00
parent 8a97e3fbe2
commit b2029b6c0f
336 changed files with 9241 additions and 2537 deletions
+131
View File
@@ -0,0 +1,131 @@
"""POS inventory sync. Inbound webhooks decrement shop stock; outbound queues reserve POS."""
from __future__ import annotations
import logging
import requests
from django.conf import settings
from django.db import transaction
from pos_sync.models import POSConnection, SyncEvent
from shop.models import Product
from shop.services import ShopError, adjust_stock
logger = logging.getLogger(__name__)
class POSSyncError(RuntimeError):
pass
def active_connection() -> POSConnection | None:
return POSConnection.objects.filter(is_active=True).order_by("created_at").first()
def apply_inbound_sale(
*,
sku: str,
quantity: int,
external_id: str = "",
payload: dict | None = None,
connection: POSConnection | None = None,
) -> SyncEvent:
sku = (sku or "").strip()
if not sku:
raise POSSyncError("SKU is required.")
if quantity < 1:
raise POSSyncError("Quantity must be at least 1.")
product = Product.objects.filter(sku__iexact=sku).first()
if product is None:
raise POSSyncError(f"Unknown SKU {sku}.")
event = SyncEvent.objects.create(
connection=connection or active_connection(),
direction=SyncEvent.Direction.INBOUND,
status=SyncEvent.Status.PENDING,
sku=product.sku,
quantity=quantity,
external_id=external_id,
payload=payload or {},
)
try:
with transaction.atomic():
locked = Product.objects.select_for_update().get(pk=product.pk)
if locked.track_inventory and locked.fulfillment == Product.Fulfillment.STOCKED:
adjust_stock(locked, -quantity)
event.status = SyncEvent.Status.DONE
event.save(update_fields=["status", "updated_at"])
except ShopError as exc:
event.status = SyncEvent.Status.FAILED
event.error = str(exc)
event.save(update_fields=["status", "error", "updated_at"])
raise POSSyncError(str(exc)) from exc
return event
def enqueue_online_sale(order) -> list[SyncEvent]:
"""Queue outbound POS reserves after an online shop sale."""
events = []
connection = active_connection()
for item in order.items.all():
sku = item.sku
if not sku:
continue
events.append(
SyncEvent.objects.create(
connection=connection,
direction=SyncEvent.Direction.OUTBOUND,
status=SyncEvent.Status.PENDING,
sku=sku,
quantity=item.quantity,
external_id=order.number,
payload={"order_id": str(order.pk), "order_number": order.number},
)
)
return events
def _post_reserve(connection: POSConnection, event: SyncEvent) -> None:
base = (connection.api_base_url or settings.POS_API_BASE_URL or "").rstrip("/")
token = connection.api_token or (settings.POS_API_TOKEN or "")
if not base:
raise POSSyncError("POS API base URL is not configured.")
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
response = requests.post(
f"{base}/inventory/reserve",
json={
"sku": event.sku,
"quantity": event.quantity,
"external_id": event.external_id,
"source": "online",
},
headers=headers,
timeout=15,
)
response.raise_for_status()
def dispatch_pending_outbound() -> int:
sent = 0
for event in SyncEvent.objects.filter(
direction=SyncEvent.Direction.OUTBOUND,
status=SyncEvent.Status.PENDING,
)[:50]:
connection = event.connection or active_connection()
try:
if connection is None:
raise POSSyncError("No active POS connection.")
_post_reserve(connection, event)
except Exception as exc: # noqa: BLE001
logger.exception("POS outbound failed for %s", event.sku)
event.status = SyncEvent.Status.FAILED
event.error = str(exc)
event.save(update_fields=["status", "error", "updated_at"])
else:
event.status = SyncEvent.Status.DONE
event.error = ""
event.save(update_fields=["status", "error", "updated_at"])
sent += 1
return sent