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

## 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
This commit was merged in pull request #2.
This commit is contained in:
2026-09-06 18:40:42 -07:00
parent 8a97e3fbe2
commit 1ca5a757d9
339 changed files with 9761 additions and 2541 deletions
+61
View File
@@ -0,0 +1,61 @@
from django.db import models
from core.models import TimeStampedModel, UUIDPrimaryKeyModel
class POSConnection(UUIDPrimaryKeyModel, TimeStampedModel):
class Provider(models.TextChoices):
GENERIC = "generic", "Generic REST"
SQUARE = "square", "Square"
LIGHTSPEED = "lightspeed", "Lightspeed"
BINDERPOS = "binderpos", "BinderPOS"
CRYSTALCOMMERCE = "crystalcommerce", "CrystalCommerce"
name = models.CharField(max_length=120)
provider = models.CharField(
max_length=32, choices=Provider.choices, default=Provider.GENERIC
)
api_base_url = models.URLField(blank=True)
api_token = models.CharField(max_length=255, blank=True)
webhook_secret = models.CharField(max_length=255, blank=True)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["name"]
def __str__(self) -> str:
return self.name
class SyncEvent(UUIDPrimaryKeyModel, TimeStampedModel):
class Direction(models.TextChoices):
INBOUND = "inbound", "POS → site"
OUTBOUND = "outbound", "Site → POS"
class Status(models.TextChoices):
PENDING = "pending", "Pending"
DONE = "done", "Done"
FAILED = "failed", "Failed"
connection = models.ForeignKey(
POSConnection,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="events",
)
direction = models.CharField(max_length=16, choices=Direction.choices)
status = models.CharField(
max_length=16, choices=Status.choices, default=Status.PENDING
)
sku = models.CharField(max_length=64)
quantity = models.IntegerField(default=0)
external_id = models.CharField(max_length=255, blank=True)
payload = models.JSONField(default=dict, blank=True)
error = models.TextField(blank=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return f"{self.direction} {self.sku} × {self.quantity} · {self.status}"