From c953232f3c66bf3b73d76950418946e46a02f42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 9 Mar 2026 19:50:00 +0100 Subject: [PATCH] feat(back-end) entities for shop --- .../controller/QuoteSessionController.java | 1 + .../dto/AdminQuoteSessionDto.java | 9 + .../com/printcalculator/dto/OrderDto.java | 4 + .../com/printcalculator/dto/OrderItemDto.java | 36 +++ .../com/printcalculator/entity/Order.java | 40 +++ .../com/printcalculator/entity/OrderItem.java | 110 ++++++++ .../printcalculator/entity/QuoteLineItem.java | 139 ++++++++++ .../printcalculator/entity/QuoteSession.java | 25 ++ .../printcalculator/entity/ShopCategory.java | 221 ++++++++++++++++ .../printcalculator/entity/ShopProduct.java | 250 ++++++++++++++++++ .../entity/ShopProductModelAsset.java | 189 +++++++++++++ .../entity/ShopProductVariant.java | 218 +++++++++++++++ .../repository/ShopCategoryRepository.java | 16 ++ .../ShopProductModelAssetRepository.java | 11 + .../repository/ShopProductRepository.java | 18 ++ .../ShopProductVariantRepository.java | 16 ++ .../printcalculator/service/OrderService.java | 21 ++ .../AdminOperationsControllerService.java | 2 + .../order/AdminOrderControllerService.java | 18 ++ .../service/order/OrderControllerService.java | 18 ++ .../quote/QuoteSessionItemService.java | 2 + .../quote/QuoteSessionResponseAssembler.java | 14 + db.sql | 233 ++++++++++++++++ 23 files changed, 1611 insertions(+) create mode 100644 backend/src/main/java/com/printcalculator/entity/ShopCategory.java create mode 100644 backend/src/main/java/com/printcalculator/entity/ShopProduct.java create mode 100644 backend/src/main/java/com/printcalculator/entity/ShopProductModelAsset.java create mode 100644 backend/src/main/java/com/printcalculator/entity/ShopProductVariant.java create mode 100644 backend/src/main/java/com/printcalculator/repository/ShopCategoryRepository.java create mode 100644 backend/src/main/java/com/printcalculator/repository/ShopProductModelAssetRepository.java create mode 100644 backend/src/main/java/com/printcalculator/repository/ShopProductRepository.java create mode 100644 backend/src/main/java/com/printcalculator/repository/ShopProductVariantRepository.java diff --git a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java index 5f13e46..cde3605 100644 --- a/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java +++ b/backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java @@ -62,6 +62,7 @@ public class QuoteSessionController { public ResponseEntity createSession() { QuoteSession session = new QuoteSession(); session.setStatus("ACTIVE"); + session.setSessionType("PRINT_QUOTE"); session.setPricingVersion("v1"); session.setMaterialCode("PLA"); session.setSupportsEnabled(false); diff --git a/backend/src/main/java/com/printcalculator/dto/AdminQuoteSessionDto.java b/backend/src/main/java/com/printcalculator/dto/AdminQuoteSessionDto.java index 1b362eb..7d29ea5 100644 --- a/backend/src/main/java/com/printcalculator/dto/AdminQuoteSessionDto.java +++ b/backend/src/main/java/com/printcalculator/dto/AdminQuoteSessionDto.java @@ -7,6 +7,7 @@ import java.util.UUID; public class AdminQuoteSessionDto { private UUID id; private String status; + private String sessionType; private String materialCode; private OffsetDateTime createdAt; private OffsetDateTime expiresAt; @@ -32,6 +33,14 @@ public class AdminQuoteSessionDto { this.status = status; } + public String getSessionType() { + return sessionType; + } + + public void setSessionType(String sessionType) { + this.sessionType = sessionType; + } + public String getMaterialCode() { return materialCode; } diff --git a/backend/src/main/java/com/printcalculator/dto/OrderDto.java b/backend/src/main/java/com/printcalculator/dto/OrderDto.java index 9653d99..4534d37 100644 --- a/backend/src/main/java/com/printcalculator/dto/OrderDto.java +++ b/backend/src/main/java/com/printcalculator/dto/OrderDto.java @@ -8,6 +8,7 @@ import java.util.UUID; public class OrderDto { private UUID id; private String orderNumber; + private String sourceType; private String status; private String paymentStatus; private String paymentMethod; @@ -45,6 +46,9 @@ public class OrderDto { public String getOrderNumber() { return orderNumber; } public void setOrderNumber(String orderNumber) { this.orderNumber = orderNumber; } + public String getSourceType() { return sourceType; } + public void setSourceType(String sourceType) { this.sourceType = sourceType; } + public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } diff --git a/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java b/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java index d6f5f68..efbcc87 100644 --- a/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java +++ b/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java @@ -5,10 +5,19 @@ import java.util.UUID; public class OrderItemDto { private UUID id; + private String itemType; private String originalFilename; + private String displayName; private String materialCode; private String colorCode; private Long filamentVariantId; + private UUID shopProductId; + private UUID shopProductVariantId; + private String shopProductSlug; + private String shopProductName; + private String shopVariantLabel; + private String shopVariantColorName; + private String shopVariantColorHex; private String filamentVariantDisplayName; private String filamentColorName; private String filamentColorHex; @@ -28,9 +37,15 @@ public class OrderItemDto { public UUID getId() { return id; } public void setId(UUID id) { this.id = id; } + public String getItemType() { return itemType; } + public void setItemType(String itemType) { this.itemType = itemType; } + public String getOriginalFilename() { return originalFilename; } public void setOriginalFilename(String originalFilename) { this.originalFilename = originalFilename; } + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + public String getMaterialCode() { return materialCode; } public void setMaterialCode(String materialCode) { this.materialCode = materialCode; } @@ -40,6 +55,27 @@ public class OrderItemDto { public Long getFilamentVariantId() { return filamentVariantId; } public void setFilamentVariantId(Long filamentVariantId) { this.filamentVariantId = filamentVariantId; } + public UUID getShopProductId() { return shopProductId; } + public void setShopProductId(UUID shopProductId) { this.shopProductId = shopProductId; } + + public UUID getShopProductVariantId() { return shopProductVariantId; } + public void setShopProductVariantId(UUID shopProductVariantId) { this.shopProductVariantId = shopProductVariantId; } + + public String getShopProductSlug() { return shopProductSlug; } + public void setShopProductSlug(String shopProductSlug) { this.shopProductSlug = shopProductSlug; } + + public String getShopProductName() { return shopProductName; } + public void setShopProductName(String shopProductName) { this.shopProductName = shopProductName; } + + public String getShopVariantLabel() { return shopVariantLabel; } + public void setShopVariantLabel(String shopVariantLabel) { this.shopVariantLabel = shopVariantLabel; } + + public String getShopVariantColorName() { return shopVariantColorName; } + public void setShopVariantColorName(String shopVariantColorName) { this.shopVariantColorName = shopVariantColorName; } + + public String getShopVariantColorHex() { return shopVariantColorHex; } + public void setShopVariantColorHex(String shopVariantColorHex) { this.shopVariantColorHex = shopVariantColorHex; } + public String getFilamentVariantDisplayName() { return filamentVariantDisplayName; } public void setFilamentVariantDisplayName(String filamentVariantDisplayName) { this.filamentVariantDisplayName = filamentVariantDisplayName; } diff --git a/backend/src/main/java/com/printcalculator/entity/Order.java b/backend/src/main/java/com/printcalculator/entity/Order.java index 1b01df1..71ec184 100644 --- a/backend/src/main/java/com/printcalculator/entity/Order.java +++ b/backend/src/main/java/com/printcalculator/entity/Order.java @@ -20,6 +20,10 @@ public class Order { @JoinColumn(name = "source_quote_session_id") private QuoteSession sourceQuoteSession; + @ColumnDefault("'CALCULATOR'") + @Column(name = "source_type", nullable = false, length = Integer.MAX_VALUE) + private String sourceType; + @Column(name = "status", nullable = false, length = Integer.MAX_VALUE) private String status; @@ -151,6 +155,34 @@ public class Order { @Column(name = "paid_at") private OffsetDateTime paidAt; + @PrePersist + private void onCreate() { + OffsetDateTime now = OffsetDateTime.now(); + if (createdAt == null) { + createdAt = now; + } + if (updatedAt == null) { + updatedAt = now; + } + if (shippingSameAsBilling == null) { + shippingSameAsBilling = true; + } + if (sourceType == null || sourceType.isBlank()) { + sourceType = "CALCULATOR"; + } + } + + @PreUpdate + private void onUpdate() { + updatedAt = OffsetDateTime.now(); + if (shippingSameAsBilling == null) { + shippingSameAsBilling = true; + } + if (sourceType == null || sourceType.isBlank()) { + sourceType = "CALCULATOR"; + } + } + public UUID getId() { return id; } @@ -177,6 +209,14 @@ public class Order { this.sourceQuoteSession = sourceQuoteSession; } + public String getSourceType() { + return sourceType; + } + + public void setSourceType(String sourceType) { + this.sourceType = sourceType; + } + public String getStatus() { return status; } diff --git a/backend/src/main/java/com/printcalculator/entity/OrderItem.java b/backend/src/main/java/com/printcalculator/entity/OrderItem.java index b77573d..e9035c5 100644 --- a/backend/src/main/java/com/printcalculator/entity/OrderItem.java +++ b/backend/src/main/java/com/printcalculator/entity/OrderItem.java @@ -23,9 +23,16 @@ public class OrderItem { @JoinColumn(name = "order_id", nullable = false) private Order order; + @ColumnDefault("'PRINT_FILE'") + @Column(name = "item_type", nullable = false, length = Integer.MAX_VALUE) + private String itemType; + @Column(name = "original_filename", nullable = false, length = Integer.MAX_VALUE) private String originalFilename; + @Column(name = "display_name", length = Integer.MAX_VALUE) + private String displayName; + @Column(name = "stored_relative_path", nullable = false, length = Integer.MAX_VALUE) private String storedRelativePath; @@ -66,6 +73,29 @@ public class OrderItem { @JoinColumn(name = "filament_variant_id") private FilamentVariant filamentVariant; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "shop_product_id") + private ShopProduct shopProduct; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "shop_product_variant_id") + private ShopProductVariant shopProductVariant; + + @Column(name = "shop_product_slug", length = Integer.MAX_VALUE) + private String shopProductSlug; + + @Column(name = "shop_product_name", length = Integer.MAX_VALUE) + private String shopProductName; + + @Column(name = "shop_variant_label", length = Integer.MAX_VALUE) + private String shopVariantLabel; + + @Column(name = "shop_variant_color_name", length = Integer.MAX_VALUE) + private String shopVariantColorName; + + @Column(name = "shop_variant_color_hex", length = Integer.MAX_VALUE) + private String shopVariantColorHex; + @Column(name = "color_code", length = Integer.MAX_VALUE) private String colorCode; @@ -106,6 +136,14 @@ public class OrderItem { if (quantity == null) { quantity = 1; } + if (itemType == null || itemType.isBlank()) { + itemType = "PRINT_FILE"; + } + if ((displayName == null || displayName.isBlank()) && originalFilename != null && !originalFilename.isBlank()) { + displayName = originalFilename; + } else if ((displayName == null || displayName.isBlank()) && shopProductName != null && !shopProductName.isBlank()) { + displayName = shopProductName; + } } public UUID getId() { @@ -124,6 +162,14 @@ public class OrderItem { this.order = order; } + public String getItemType() { + return itemType; + } + + public void setItemType(String itemType) { + this.itemType = itemType; + } + public String getOriginalFilename() { return originalFilename; } @@ -132,6 +178,14 @@ public class OrderItem { this.originalFilename = originalFilename; } + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + public String getStoredRelativePath() { return storedRelativePath; } @@ -236,6 +290,62 @@ public class OrderItem { this.filamentVariant = filamentVariant; } + public ShopProduct getShopProduct() { + return shopProduct; + } + + public void setShopProduct(ShopProduct shopProduct) { + this.shopProduct = shopProduct; + } + + public ShopProductVariant getShopProductVariant() { + return shopProductVariant; + } + + public void setShopProductVariant(ShopProductVariant shopProductVariant) { + this.shopProductVariant = shopProductVariant; + } + + public String getShopProductSlug() { + return shopProductSlug; + } + + public void setShopProductSlug(String shopProductSlug) { + this.shopProductSlug = shopProductSlug; + } + + public String getShopProductName() { + return shopProductName; + } + + public void setShopProductName(String shopProductName) { + this.shopProductName = shopProductName; + } + + public String getShopVariantLabel() { + return shopVariantLabel; + } + + public void setShopVariantLabel(String shopVariantLabel) { + this.shopVariantLabel = shopVariantLabel; + } + + public String getShopVariantColorName() { + return shopVariantColorName; + } + + public void setShopVariantColorName(String shopVariantColorName) { + this.shopVariantColorName = shopVariantColorName; + } + + public String getShopVariantColorHex() { + return shopVariantColorHex; + } + + public void setShopVariantColorHex(String shopVariantColorHex) { + this.shopVariantColorHex = shopVariantColorHex; + } + public String getColorCode() { return colorCode; } diff --git a/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java b/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java index 4103f8c..f1e3042 100644 --- a/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java +++ b/backend/src/main/java/com/printcalculator/entity/QuoteLineItem.java @@ -30,9 +30,16 @@ public class QuoteLineItem { @Column(name = "status", nullable = false, length = Integer.MAX_VALUE) private String status; + @ColumnDefault("'PRINT_FILE'") + @Column(name = "line_item_type", nullable = false, length = Integer.MAX_VALUE) + private String lineItemType; + @Column(name = "original_filename", nullable = false, length = Integer.MAX_VALUE) private String originalFilename; + @Column(name = "display_name", length = Integer.MAX_VALUE) + private String displayName; + @ColumnDefault("1") @Column(name = "quantity", nullable = false) private Integer quantity; @@ -45,6 +52,31 @@ public class QuoteLineItem { @com.fasterxml.jackson.annotation.JsonIgnore private FilamentVariant filamentVariant; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "shop_product_id") + @com.fasterxml.jackson.annotation.JsonIgnore + private ShopProduct shopProduct; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "shop_product_variant_id") + @com.fasterxml.jackson.annotation.JsonIgnore + private ShopProductVariant shopProductVariant; + + @Column(name = "shop_product_slug", length = Integer.MAX_VALUE) + private String shopProductSlug; + + @Column(name = "shop_product_name", length = Integer.MAX_VALUE) + private String shopProductName; + + @Column(name = "shop_variant_label", length = Integer.MAX_VALUE) + private String shopVariantLabel; + + @Column(name = "shop_variant_color_name", length = Integer.MAX_VALUE) + private String shopVariantColorName; + + @Column(name = "shop_variant_color_hex", length = Integer.MAX_VALUE) + private String shopVariantColorHex; + @Column(name = "material_code", length = Integer.MAX_VALUE) private String materialCode; @@ -102,6 +134,41 @@ public class QuoteLineItem { @Column(name = "updated_at", nullable = false) private OffsetDateTime updatedAt; + @PrePersist + private void onCreate() { + OffsetDateTime now = OffsetDateTime.now(); + if (createdAt == null) { + createdAt = now; + } + if (updatedAt == null) { + updatedAt = now; + } + if (quantity == null) { + quantity = 1; + } + if (lineItemType == null || lineItemType.isBlank()) { + lineItemType = "PRINT_FILE"; + } + if ((displayName == null || displayName.isBlank()) && originalFilename != null && !originalFilename.isBlank()) { + displayName = originalFilename; + } else if ((displayName == null || displayName.isBlank()) && shopProductName != null && !shopProductName.isBlank()) { + displayName = shopProductName; + } + } + + @PreUpdate + private void onUpdate() { + updatedAt = OffsetDateTime.now(); + if (lineItemType == null || lineItemType.isBlank()) { + lineItemType = "PRINT_FILE"; + } + if ((displayName == null || displayName.isBlank()) && originalFilename != null && !originalFilename.isBlank()) { + displayName = originalFilename; + } else if ((displayName == null || displayName.isBlank()) && shopProductName != null && !shopProductName.isBlank()) { + displayName = shopProductName; + } + } + public UUID getId() { return id; } @@ -126,6 +193,14 @@ public class QuoteLineItem { this.status = status; } + public String getLineItemType() { + return lineItemType; + } + + public void setLineItemType(String lineItemType) { + this.lineItemType = lineItemType; + } + public String getOriginalFilename() { return originalFilename; } @@ -134,6 +209,14 @@ public class QuoteLineItem { this.originalFilename = originalFilename; } + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + public Integer getQuantity() { return quantity; } @@ -158,6 +241,62 @@ public class QuoteLineItem { this.filamentVariant = filamentVariant; } + public ShopProduct getShopProduct() { + return shopProduct; + } + + public void setShopProduct(ShopProduct shopProduct) { + this.shopProduct = shopProduct; + } + + public ShopProductVariant getShopProductVariant() { + return shopProductVariant; + } + + public void setShopProductVariant(ShopProductVariant shopProductVariant) { + this.shopProductVariant = shopProductVariant; + } + + public String getShopProductSlug() { + return shopProductSlug; + } + + public void setShopProductSlug(String shopProductSlug) { + this.shopProductSlug = shopProductSlug; + } + + public String getShopProductName() { + return shopProductName; + } + + public void setShopProductName(String shopProductName) { + this.shopProductName = shopProductName; + } + + public String getShopVariantLabel() { + return shopVariantLabel; + } + + public void setShopVariantLabel(String shopVariantLabel) { + this.shopVariantLabel = shopVariantLabel; + } + + public String getShopVariantColorName() { + return shopVariantColorName; + } + + public void setShopVariantColorName(String shopVariantColorName) { + this.shopVariantColorName = shopVariantColorName; + } + + public String getShopVariantColorHex() { + return shopVariantColorHex; + } + + public void setShopVariantColorHex(String shopVariantColorHex) { + this.shopVariantColorHex = shopVariantColorHex; + } + public String getMaterialCode() { return materialCode; } diff --git a/backend/src/main/java/com/printcalculator/entity/QuoteSession.java b/backend/src/main/java/com/printcalculator/entity/QuoteSession.java index e9746ef..6b0faad 100644 --- a/backend/src/main/java/com/printcalculator/entity/QuoteSession.java +++ b/backend/src/main/java/com/printcalculator/entity/QuoteSession.java @@ -22,6 +22,10 @@ public class QuoteSession { @Column(name = "status", nullable = false, length = Integer.MAX_VALUE) private String status; + @ColumnDefault("'PRINT_QUOTE'") + @Column(name = "session_type", nullable = false, length = Integer.MAX_VALUE) + private String sessionType; + @Column(name = "pricing_version", nullable = false, length = Integer.MAX_VALUE) private String pricingVersion; @@ -70,6 +74,19 @@ public class QuoteSession { @Column(name = "cad_hourly_rate_chf", precision = 10, scale = 2) private BigDecimal cadHourlyRateChf; + @PrePersist + private void onCreate() { + if (sessionType == null || sessionType.isBlank()) { + sessionType = "PRINT_QUOTE"; + } + if (supportsEnabled == null) { + supportsEnabled = false; + } + if (createdAt == null) { + createdAt = OffsetDateTime.now(); + } + } + public UUID getId() { return id; } @@ -86,6 +103,14 @@ public class QuoteSession { this.status = status; } + public String getSessionType() { + return sessionType; + } + + public void setSessionType(String sessionType) { + this.sessionType = sessionType; + } + public String getPricingVersion() { return pricingVersion; } diff --git a/backend/src/main/java/com/printcalculator/entity/ShopCategory.java b/backend/src/main/java/com/printcalculator/entity/ShopCategory.java new file mode 100644 index 0000000..a018a97 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/entity/ShopCategory.java @@ -0,0 +1,221 @@ +package com.printcalculator.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import org.hibernate.annotations.ColumnDefault; + +import java.time.OffsetDateTime; +import java.util.UUID; + +@Entity +@Table(name = "shop_category", indexes = { + @Index(name = "ix_shop_category_parent_sort", columnList = "parent_category_id, sort_order"), + @Index(name = "ix_shop_category_active_sort", columnList = "is_active, sort_order") +}) +public class ShopCategory { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "shop_category_id", nullable = false) + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_category_id") + private ShopCategory parentCategory; + + @Column(name = "slug", nullable = false, unique = true, length = Integer.MAX_VALUE) + private String slug; + + @Column(name = "name", nullable = false, length = Integer.MAX_VALUE) + private String name; + + @Column(name = "description", length = Integer.MAX_VALUE) + private String description; + + @Column(name = "seo_title", length = Integer.MAX_VALUE) + private String seoTitle; + + @Column(name = "seo_description", length = Integer.MAX_VALUE) + private String seoDescription; + + @Column(name = "og_title", length = Integer.MAX_VALUE) + private String ogTitle; + + @Column(name = "og_description", length = Integer.MAX_VALUE) + private String ogDescription; + + @ColumnDefault("true") + @Column(name = "indexable", nullable = false) + private Boolean indexable; + + @ColumnDefault("true") + @Column(name = "is_active", nullable = false) + private Boolean isActive; + + @ColumnDefault("0") + @Column(name = "sort_order", nullable = false) + private Integer sortOrder; + + @ColumnDefault("now()") + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt; + + @ColumnDefault("now()") + @Column(name = "updated_at", nullable = false) + private OffsetDateTime updatedAt; + + @PrePersist + private void onCreate() { + OffsetDateTime now = OffsetDateTime.now(); + if (createdAt == null) { + createdAt = now; + } + if (updatedAt == null) { + updatedAt = now; + } + if (indexable == null) { + indexable = true; + } + if (isActive == null) { + isActive = true; + } + if (sortOrder == null) { + sortOrder = 0; + } + } + + @PreUpdate + private void onUpdate() { + updatedAt = OffsetDateTime.now(); + if (indexable == null) { + indexable = true; + } + if (isActive == null) { + isActive = true; + } + if (sortOrder == null) { + sortOrder = 0; + } + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public ShopCategory getParentCategory() { + return parentCategory; + } + + public void setParentCategory(ShopCategory parentCategory) { + this.parentCategory = parentCategory; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getSeoTitle() { + return seoTitle; + } + + public void setSeoTitle(String seoTitle) { + this.seoTitle = seoTitle; + } + + public String getSeoDescription() { + return seoDescription; + } + + public void setSeoDescription(String seoDescription) { + this.seoDescription = seoDescription; + } + + public String getOgTitle() { + return ogTitle; + } + + public void setOgTitle(String ogTitle) { + this.ogTitle = ogTitle; + } + + public String getOgDescription() { + return ogDescription; + } + + public void setOgDescription(String ogDescription) { + this.ogDescription = ogDescription; + } + + public Boolean getIndexable() { + return indexable; + } + + public void setIndexable(Boolean indexable) { + this.indexable = indexable; + } + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean active) { + isActive = active; + } + + public Integer getSortOrder() { + return sortOrder; + } + + public void setSortOrder(Integer sortOrder) { + this.sortOrder = sortOrder; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(OffsetDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/printcalculator/entity/ShopProduct.java b/backend/src/main/java/com/printcalculator/entity/ShopProduct.java new file mode 100644 index 0000000..ea8211e --- /dev/null +++ b/backend/src/main/java/com/printcalculator/entity/ShopProduct.java @@ -0,0 +1,250 @@ +package com.printcalculator.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import org.hibernate.annotations.ColumnDefault; + +import java.time.OffsetDateTime; +import java.util.UUID; + +@Entity +@Table(name = "shop_product", indexes = { + @Index(name = "ix_shop_product_category_active_sort", columnList = "shop_category_id, is_active, sort_order"), + @Index(name = "ix_shop_product_featured_sort", columnList = "is_featured, is_active, sort_order") +}) +public class ShopProduct { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "shop_product_id", nullable = false) + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "shop_category_id", nullable = false) + private ShopCategory category; + + @Column(name = "slug", nullable = false, unique = true, length = Integer.MAX_VALUE) + private String slug; + + @Column(name = "name", nullable = false, length = Integer.MAX_VALUE) + private String name; + + @Column(name = "excerpt", length = Integer.MAX_VALUE) + private String excerpt; + + @Column(name = "description", length = Integer.MAX_VALUE) + private String description; + + @Column(name = "seo_title", length = Integer.MAX_VALUE) + private String seoTitle; + + @Column(name = "seo_description", length = Integer.MAX_VALUE) + private String seoDescription; + + @Column(name = "og_title", length = Integer.MAX_VALUE) + private String ogTitle; + + @Column(name = "og_description", length = Integer.MAX_VALUE) + private String ogDescription; + + @ColumnDefault("true") + @Column(name = "indexable", nullable = false) + private Boolean indexable; + + @ColumnDefault("false") + @Column(name = "is_featured", nullable = false) + private Boolean isFeatured; + + @ColumnDefault("true") + @Column(name = "is_active", nullable = false) + private Boolean isActive; + + @ColumnDefault("0") + @Column(name = "sort_order", nullable = false) + private Integer sortOrder; + + @ColumnDefault("now()") + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt; + + @ColumnDefault("now()") + @Column(name = "updated_at", nullable = false) + private OffsetDateTime updatedAt; + + @PrePersist + private void onCreate() { + OffsetDateTime now = OffsetDateTime.now(); + if (createdAt == null) { + createdAt = now; + } + if (updatedAt == null) { + updatedAt = now; + } + if (indexable == null) { + indexable = true; + } + if (isFeatured == null) { + isFeatured = false; + } + if (isActive == null) { + isActive = true; + } + if (sortOrder == null) { + sortOrder = 0; + } + } + + @PreUpdate + private void onUpdate() { + updatedAt = OffsetDateTime.now(); + if (indexable == null) { + indexable = true; + } + if (isFeatured == null) { + isFeatured = false; + } + if (isActive == null) { + isActive = true; + } + if (sortOrder == null) { + sortOrder = 0; + } + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public ShopCategory getCategory() { + return category; + } + + public void setCategory(ShopCategory category) { + this.category = category; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getExcerpt() { + return excerpt; + } + + public void setExcerpt(String excerpt) { + this.excerpt = excerpt; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getSeoTitle() { + return seoTitle; + } + + public void setSeoTitle(String seoTitle) { + this.seoTitle = seoTitle; + } + + public String getSeoDescription() { + return seoDescription; + } + + public void setSeoDescription(String seoDescription) { + this.seoDescription = seoDescription; + } + + public String getOgTitle() { + return ogTitle; + } + + public void setOgTitle(String ogTitle) { + this.ogTitle = ogTitle; + } + + public String getOgDescription() { + return ogDescription; + } + + public void setOgDescription(String ogDescription) { + this.ogDescription = ogDescription; + } + + public Boolean getIndexable() { + return indexable; + } + + public void setIndexable(Boolean indexable) { + this.indexable = indexable; + } + + public Boolean getIsFeatured() { + return isFeatured; + } + + public void setIsFeatured(Boolean featured) { + isFeatured = featured; + } + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean active) { + isActive = active; + } + + public Integer getSortOrder() { + return sortOrder; + } + + public void setSortOrder(Integer sortOrder) { + this.sortOrder = sortOrder; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(OffsetDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/printcalculator/entity/ShopProductModelAsset.java b/backend/src/main/java/com/printcalculator/entity/ShopProductModelAsset.java new file mode 100644 index 0000000..9287281 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/entity/ShopProductModelAsset.java @@ -0,0 +1,189 @@ +package com.printcalculator.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import org.hibernate.annotations.ColumnDefault; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.UUID; + +@Entity +@Table(name = "shop_product_model_asset", indexes = { + @Index(name = "ix_shop_product_model_asset_product", columnList = "shop_product_id") +}) +public class ShopProductModelAsset { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "shop_product_model_asset_id", nullable = false) + private UUID id; + + @OneToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "shop_product_id", nullable = false, unique = true) + private ShopProduct product; + + @Column(name = "original_filename", nullable = false, length = Integer.MAX_VALUE) + private String originalFilename; + + @Column(name = "stored_relative_path", nullable = false, length = Integer.MAX_VALUE) + private String storedRelativePath; + + @Column(name = "stored_filename", nullable = false, length = Integer.MAX_VALUE) + private String storedFilename; + + @Column(name = "file_size_bytes") + private Long fileSizeBytes; + + @Column(name = "mime_type", length = Integer.MAX_VALUE) + private String mimeType; + + @Column(name = "sha256_hex", length = Integer.MAX_VALUE) + private String sha256Hex; + + @Column(name = "bounding_box_x_mm", precision = 10, scale = 3) + private BigDecimal boundingBoxXMm; + + @Column(name = "bounding_box_y_mm", precision = 10, scale = 3) + private BigDecimal boundingBoxYMm; + + @Column(name = "bounding_box_z_mm", precision = 10, scale = 3) + private BigDecimal boundingBoxZMm; + + @ColumnDefault("now()") + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt; + + @ColumnDefault("now()") + @Column(name = "updated_at", nullable = false) + private OffsetDateTime updatedAt; + + @PrePersist + private void onCreate() { + OffsetDateTime now = OffsetDateTime.now(); + if (createdAt == null) { + createdAt = now; + } + if (updatedAt == null) { + updatedAt = now; + } + } + + @PreUpdate + private void onUpdate() { + updatedAt = OffsetDateTime.now(); + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public ShopProduct getProduct() { + return product; + } + + public void setProduct(ShopProduct product) { + this.product = product; + } + + public String getOriginalFilename() { + return originalFilename; + } + + public void setOriginalFilename(String originalFilename) { + this.originalFilename = originalFilename; + } + + public String getStoredRelativePath() { + return storedRelativePath; + } + + public void setStoredRelativePath(String storedRelativePath) { + this.storedRelativePath = storedRelativePath; + } + + public String getStoredFilename() { + return storedFilename; + } + + public void setStoredFilename(String storedFilename) { + this.storedFilename = storedFilename; + } + + public Long getFileSizeBytes() { + return fileSizeBytes; + } + + public void setFileSizeBytes(Long fileSizeBytes) { + this.fileSizeBytes = fileSizeBytes; + } + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public String getSha256Hex() { + return sha256Hex; + } + + public void setSha256Hex(String sha256Hex) { + this.sha256Hex = sha256Hex; + } + + public BigDecimal getBoundingBoxXMm() { + return boundingBoxXMm; + } + + public void setBoundingBoxXMm(BigDecimal boundingBoxXMm) { + this.boundingBoxXMm = boundingBoxXMm; + } + + public BigDecimal getBoundingBoxYMm() { + return boundingBoxYMm; + } + + public void setBoundingBoxYMm(BigDecimal boundingBoxYMm) { + this.boundingBoxYMm = boundingBoxYMm; + } + + public BigDecimal getBoundingBoxZMm() { + return boundingBoxZMm; + } + + public void setBoundingBoxZMm(BigDecimal boundingBoxZMm) { + this.boundingBoxZMm = boundingBoxZMm; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(OffsetDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/printcalculator/entity/ShopProductVariant.java b/backend/src/main/java/com/printcalculator/entity/ShopProductVariant.java new file mode 100644 index 0000000..d1d6d03 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/entity/ShopProductVariant.java @@ -0,0 +1,218 @@ +package com.printcalculator.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import org.hibernate.annotations.ColumnDefault; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.UUID; + +@Entity +@Table(name = "shop_product_variant", indexes = { + @Index(name = "ix_shop_product_variant_product_active_sort", columnList = "shop_product_id, is_active, sort_order"), + @Index(name = "ix_shop_product_variant_sku", columnList = "sku") +}) +public class ShopProductVariant { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "shop_product_variant_id", nullable = false) + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "shop_product_id", nullable = false) + private ShopProduct product; + + @Column(name = "sku", unique = true, length = Integer.MAX_VALUE) + private String sku; + + @Column(name = "variant_label", nullable = false, length = Integer.MAX_VALUE) + private String variantLabel; + + @Column(name = "color_name", nullable = false, length = Integer.MAX_VALUE) + private String colorName; + + @Column(name = "color_hex", length = Integer.MAX_VALUE) + private String colorHex; + + @Column(name = "internal_material_code", nullable = false, length = Integer.MAX_VALUE) + private String internalMaterialCode; + + @ColumnDefault("0.00") + @Column(name = "price_chf", nullable = false, precision = 12, scale = 2) + private BigDecimal priceChf; + + @ColumnDefault("false") + @Column(name = "is_default", nullable = false) + private Boolean isDefault; + + @ColumnDefault("true") + @Column(name = "is_active", nullable = false) + private Boolean isActive; + + @ColumnDefault("0") + @Column(name = "sort_order", nullable = false) + private Integer sortOrder; + + @ColumnDefault("now()") + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt; + + @ColumnDefault("now()") + @Column(name = "updated_at", nullable = false) + private OffsetDateTime updatedAt; + + @PrePersist + private void onCreate() { + OffsetDateTime now = OffsetDateTime.now(); + if (createdAt == null) { + createdAt = now; + } + if (updatedAt == null) { + updatedAt = now; + } + if (priceChf == null) { + priceChf = BigDecimal.ZERO; + } + if (isDefault == null) { + isDefault = false; + } + if (isActive == null) { + isActive = true; + } + if (sortOrder == null) { + sortOrder = 0; + } + } + + @PreUpdate + private void onUpdate() { + updatedAt = OffsetDateTime.now(); + if (priceChf == null) { + priceChf = BigDecimal.ZERO; + } + if (isDefault == null) { + isDefault = false; + } + if (isActive == null) { + isActive = true; + } + if (sortOrder == null) { + sortOrder = 0; + } + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public ShopProduct getProduct() { + return product; + } + + public void setProduct(ShopProduct product) { + this.product = product; + } + + public String getSku() { + return sku; + } + + public void setSku(String sku) { + this.sku = sku; + } + + public String getVariantLabel() { + return variantLabel; + } + + public void setVariantLabel(String variantLabel) { + this.variantLabel = variantLabel; + } + + public String getColorName() { + return colorName; + } + + public void setColorName(String colorName) { + this.colorName = colorName; + } + + public String getColorHex() { + return colorHex; + } + + public void setColorHex(String colorHex) { + this.colorHex = colorHex; + } + + public String getInternalMaterialCode() { + return internalMaterialCode; + } + + public void setInternalMaterialCode(String internalMaterialCode) { + this.internalMaterialCode = internalMaterialCode; + } + + public BigDecimal getPriceChf() { + return priceChf; + } + + public void setPriceChf(BigDecimal priceChf) { + this.priceChf = priceChf; + } + + public Boolean getIsDefault() { + return isDefault; + } + + public void setIsDefault(Boolean aDefault) { + isDefault = aDefault; + } + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean active) { + isActive = active; + } + + public Integer getSortOrder() { + return sortOrder; + } + + public void setSortOrder(Integer sortOrder) { + this.sortOrder = sortOrder; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(OffsetDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/printcalculator/repository/ShopCategoryRepository.java b/backend/src/main/java/com/printcalculator/repository/ShopCategoryRepository.java new file mode 100644 index 0000000..ccc6066 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/repository/ShopCategoryRepository.java @@ -0,0 +1,16 @@ +package com.printcalculator.repository; + +import com.printcalculator.entity.ShopCategory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface ShopCategoryRepository extends JpaRepository { + Optional findBySlug(String slug); + + boolean existsBySlugIgnoreCase(String slug); + + List findAllByOrderBySortOrderAscNameAsc(); +} diff --git a/backend/src/main/java/com/printcalculator/repository/ShopProductModelAssetRepository.java b/backend/src/main/java/com/printcalculator/repository/ShopProductModelAssetRepository.java new file mode 100644 index 0000000..c278be6 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/repository/ShopProductModelAssetRepository.java @@ -0,0 +1,11 @@ +package com.printcalculator.repository; + +import com.printcalculator.entity.ShopProductModelAsset; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface ShopProductModelAssetRepository extends JpaRepository { + Optional findByProduct_Id(UUID productId); +} diff --git a/backend/src/main/java/com/printcalculator/repository/ShopProductRepository.java b/backend/src/main/java/com/printcalculator/repository/ShopProductRepository.java new file mode 100644 index 0000000..832d433 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/repository/ShopProductRepository.java @@ -0,0 +1,18 @@ +package com.printcalculator.repository; + +import com.printcalculator.entity.ShopProduct; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface ShopProductRepository extends JpaRepository { + Optional findBySlug(String slug); + + boolean existsBySlugIgnoreCase(String slug); + + List findAllByOrderBySortOrderAscNameAsc(); + + List findByCategory_IdOrderBySortOrderAscNameAsc(UUID categoryId); +} diff --git a/backend/src/main/java/com/printcalculator/repository/ShopProductVariantRepository.java b/backend/src/main/java/com/printcalculator/repository/ShopProductVariantRepository.java new file mode 100644 index 0000000..6ef842f --- /dev/null +++ b/backend/src/main/java/com/printcalculator/repository/ShopProductVariantRepository.java @@ -0,0 +1,16 @@ +package com.printcalculator.repository; + +import com.printcalculator.entity.ShopProductVariant; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface ShopProductVariantRepository extends JpaRepository { + List findByProduct_IdOrderBySortOrderAscColorNameAsc(UUID productId); + + Optional findFirstByProduct_IdAndIsDefaultTrue(UUID productId); + + boolean existsBySkuIgnoreCase(String sku); +} diff --git a/backend/src/main/java/com/printcalculator/service/OrderService.java b/backend/src/main/java/com/printcalculator/service/OrderService.java index ebef7d8..6426046 100644 --- a/backend/src/main/java/com/printcalculator/service/OrderService.java +++ b/backend/src/main/java/com/printcalculator/service/OrderService.java @@ -104,6 +104,7 @@ public class OrderService { Order order = new Order(); order.setSourceQuoteSession(session); + order.setSourceType(resolveOrderSourceType(session)); order.setCustomer(customer); order.setCustomerEmail(request.getCustomer().getEmail()); order.setCustomerPhone(request.getCustomer().getPhone()); @@ -172,11 +173,24 @@ public class OrderService { for (QuoteLineItem qItem : quoteItems) { OrderItem oItem = new OrderItem(); oItem.setOrder(order); + oItem.setItemType(qItem.getLineItemType() != null ? qItem.getLineItemType() : "PRINT_FILE"); oItem.setOriginalFilename(qItem.getOriginalFilename()); + oItem.setDisplayName( + qItem.getDisplayName() != null && !qItem.getDisplayName().isBlank() + ? qItem.getDisplayName() + : qItem.getOriginalFilename() + ); int quantity = qItem.getQuantity() != null && qItem.getQuantity() > 0 ? qItem.getQuantity() : 1; oItem.setQuantity(quantity); oItem.setColorCode(qItem.getColorCode()); oItem.setFilamentVariant(qItem.getFilamentVariant()); + oItem.setShopProduct(qItem.getShopProduct()); + oItem.setShopProductVariant(qItem.getShopProductVariant()); + oItem.setShopProductSlug(qItem.getShopProductSlug()); + oItem.setShopProductName(qItem.getShopProductName()); + oItem.setShopVariantLabel(qItem.getShopVariantLabel()); + oItem.setShopVariantColorName(qItem.getShopVariantColorName()); + oItem.setShopVariantColorHex(qItem.getShopVariantColorHex()); if (qItem.getFilamentVariant() != null && qItem.getFilamentVariant().getFilamentMaterialType() != null && qItem.getFilamentVariant().getFilamentMaterialType().getMaterialCode() != null) { @@ -319,6 +333,13 @@ public class OrderService { } } + private String resolveOrderSourceType(QuoteSession session) { + if (session != null && "SHOP_CART".equalsIgnoreCase(session.getSessionType())) { + return "SHOP"; + } + return "CALCULATOR"; + } + private String getDisplayOrderNumber(Order order) { String orderNumber = order.getOrderNumber(); if (orderNumber != null && !orderNumber.isBlank()) { diff --git a/backend/src/main/java/com/printcalculator/service/admin/AdminOperationsControllerService.java b/backend/src/main/java/com/printcalculator/service/admin/AdminOperationsControllerService.java index 1291c1a..2aef314 100644 --- a/backend/src/main/java/com/printcalculator/service/admin/AdminOperationsControllerService.java +++ b/backend/src/main/java/com/printcalculator/service/admin/AdminOperationsControllerService.java @@ -301,6 +301,7 @@ public class AdminOperationsControllerService { } else { session = new QuoteSession(); session.setStatus("CAD_ACTIVE"); + session.setSessionType("PRINT_QUOTE"); session.setPricingVersion("v1"); session.setMaterialCode("PLA"); session.setNozzleDiameterMm(BigDecimal.valueOf(0.4)); @@ -398,6 +399,7 @@ public class AdminOperationsControllerService { AdminQuoteSessionDto dto = new AdminQuoteSessionDto(); dto.setId(session.getId()); dto.setStatus(session.getStatus()); + dto.setSessionType(session.getSessionType() != null ? session.getSessionType() : "PRINT_QUOTE"); dto.setMaterialCode(session.getMaterialCode()); dto.setCreatedAt(session.getCreatedAt()); dto.setExpiresAt(session.getExpiresAt()); diff --git a/backend/src/main/java/com/printcalculator/service/order/AdminOrderControllerService.java b/backend/src/main/java/com/printcalculator/service/order/AdminOrderControllerService.java index dfda322..164ac74 100644 --- a/backend/src/main/java/com/printcalculator/service/order/AdminOrderControllerService.java +++ b/backend/src/main/java/com/printcalculator/service/order/AdminOrderControllerService.java @@ -197,6 +197,7 @@ public class AdminOrderControllerService { OrderDto dto = new OrderDto(); dto.setId(order.getId()); dto.setOrderNumber(getDisplayOrderNumber(order)); + dto.setSourceType(order.getSourceType() != null ? order.getSourceType() : "CALCULATOR"); dto.setStatus(order.getStatus()); paymentRepo.findByOrder_Id(order.getId()).ifPresent(payment -> { @@ -260,9 +261,26 @@ public class AdminOrderControllerService { List itemDtos = items.stream().map(item -> { OrderItemDto itemDto = new OrderItemDto(); itemDto.setId(item.getId()); + itemDto.setItemType(item.getItemType() != null ? item.getItemType() : "PRINT_FILE"); itemDto.setOriginalFilename(item.getOriginalFilename()); + itemDto.setDisplayName( + item.getDisplayName() != null && !item.getDisplayName().isBlank() + ? item.getDisplayName() + : item.getOriginalFilename() + ); itemDto.setMaterialCode(item.getMaterialCode()); itemDto.setColorCode(item.getColorCode()); + if (item.getShopProduct() != null) { + itemDto.setShopProductId(item.getShopProduct().getId()); + } + if (item.getShopProductVariant() != null) { + itemDto.setShopProductVariantId(item.getShopProductVariant().getId()); + } + itemDto.setShopProductSlug(item.getShopProductSlug()); + itemDto.setShopProductName(item.getShopProductName()); + itemDto.setShopVariantLabel(item.getShopVariantLabel()); + itemDto.setShopVariantColorName(item.getShopVariantColorName()); + itemDto.setShopVariantColorHex(item.getShopVariantColorHex()); if (item.getFilamentVariant() != null) { itemDto.setFilamentVariantId(item.getFilamentVariant().getId()); itemDto.setFilamentVariantDisplayName(item.getFilamentVariant().getVariantDisplayName()); diff --git a/backend/src/main/java/com/printcalculator/service/order/OrderControllerService.java b/backend/src/main/java/com/printcalculator/service/order/OrderControllerService.java index 4baca4b..9b1ae40 100644 --- a/backend/src/main/java/com/printcalculator/service/order/OrderControllerService.java +++ b/backend/src/main/java/com/printcalculator/service/order/OrderControllerService.java @@ -255,6 +255,7 @@ public class OrderControllerService { OrderDto dto = new OrderDto(); dto.setId(order.getId()); dto.setOrderNumber(getDisplayOrderNumber(order)); + dto.setSourceType(order.getSourceType() != null ? order.getSourceType() : "CALCULATOR"); dto.setStatus(order.getStatus()); paymentRepo.findByOrder_Id(order.getId()).ifPresent(payment -> { @@ -314,9 +315,26 @@ public class OrderControllerService { List itemDtos = items.stream().map(item -> { OrderItemDto itemDto = new OrderItemDto(); itemDto.setId(item.getId()); + itemDto.setItemType(item.getItemType() != null ? item.getItemType() : "PRINT_FILE"); itemDto.setOriginalFilename(item.getOriginalFilename()); + itemDto.setDisplayName( + item.getDisplayName() != null && !item.getDisplayName().isBlank() + ? item.getDisplayName() + : item.getOriginalFilename() + ); itemDto.setMaterialCode(item.getMaterialCode()); itemDto.setColorCode(item.getColorCode()); + if (item.getShopProduct() != null) { + itemDto.setShopProductId(item.getShopProduct().getId()); + } + if (item.getShopProductVariant() != null) { + itemDto.setShopProductVariantId(item.getShopProductVariant().getId()); + } + itemDto.setShopProductSlug(item.getShopProductSlug()); + itemDto.setShopProductName(item.getShopProductName()); + itemDto.setShopVariantLabel(item.getShopVariantLabel()); + itemDto.setShopVariantColorName(item.getShopVariantColorName()); + itemDto.setShopVariantColorHex(item.getShopVariantColorHex()); if (item.getFilamentVariant() != null) { itemDto.setFilamentVariantId(item.getFilamentVariant().getId()); itemDto.setFilamentVariantDisplayName(item.getFilamentVariant().getVariantDisplayName()); diff --git a/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionItemService.java b/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionItemService.java index cae1ec5..50496b3 100644 --- a/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionItemService.java +++ b/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionItemService.java @@ -237,7 +237,9 @@ public class QuoteSessionItemService { Path convertedPersistentPath) { QuoteLineItem item = new QuoteLineItem(); item.setQuoteSession(session); + item.setLineItemType("PRINT_FILE"); item.setOriginalFilename(originalFilename); + item.setDisplayName(originalFilename); item.setStoredPath(quoteStorageService.toStoredPath(persistentPath)); item.setQuantity(normalizeQuantity(settings.getQuantity())); item.setColorCode(selectedVariant.getColorName()); diff --git a/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionResponseAssembler.java b/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionResponseAssembler.java index 3652586..74ac67f 100644 --- a/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionResponseAssembler.java +++ b/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionResponseAssembler.java @@ -46,12 +46,26 @@ public class QuoteSessionResponseAssembler { private Map toItemDto(QuoteLineItem item, QuoteSessionTotalsService.QuoteSessionTotals totals) { Map dto = new HashMap<>(); dto.put("id", item.getId()); + dto.put("lineItemType", item.getLineItemType() != null ? item.getLineItemType() : "PRINT_FILE"); dto.put("originalFilename", item.getOriginalFilename()); + dto.put( + "displayName", + item.getDisplayName() != null && !item.getDisplayName().isBlank() + ? item.getDisplayName() + : item.getOriginalFilename() + ); dto.put("quantity", item.getQuantity()); dto.put("printTimeSeconds", item.getPrintTimeSeconds()); dto.put("materialGrams", item.getMaterialGrams()); dto.put("colorCode", item.getColorCode()); dto.put("filamentVariantId", item.getFilamentVariant() != null ? item.getFilamentVariant().getId() : null); + dto.put("shopProductId", item.getShopProduct() != null ? item.getShopProduct().getId() : null); + dto.put("shopProductVariantId", item.getShopProductVariant() != null ? item.getShopProductVariant().getId() : null); + dto.put("shopProductSlug", item.getShopProductSlug()); + dto.put("shopProductName", item.getShopProductName()); + dto.put("shopVariantLabel", item.getShopVariantLabel()); + dto.put("shopVariantColorName", item.getShopVariantColorName()); + dto.put("shopVariantColorHex", item.getShopVariantColorHex()); dto.put("materialCode", item.getMaterialCode()); dto.put("quality", item.getQuality()); dto.put("nozzleDiameterMm", item.getNozzleDiameterMm()); diff --git a/db.sql b/db.sql index d587f31..bc500ec 100644 --- a/db.sql +++ b/db.sql @@ -1007,6 +1007,239 @@ ALTER TABLE media_usage ALTER TABLE media_usage ADD COLUMN IF NOT EXISTS alt_text_fr text; +CREATE TABLE IF NOT EXISTS shop_category +( + shop_category_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + parent_category_id uuid REFERENCES shop_category (shop_category_id) ON DELETE SET NULL, + slug text NOT NULL UNIQUE, + name text NOT NULL, + description text, + seo_title text, + seo_description text, + og_title text, + og_description text, + indexable boolean NOT NULL DEFAULT true, + is_active boolean NOT NULL DEFAULT true, + sort_order integer NOT NULL DEFAULT 0, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT chk_shop_category_not_self_parent CHECK ( + parent_category_id IS NULL OR parent_category_id <> shop_category_id + ) +); + +CREATE INDEX IF NOT EXISTS ix_shop_category_parent_sort + ON shop_category (parent_category_id, sort_order, created_at DESC); + +CREATE INDEX IF NOT EXISTS ix_shop_category_active_sort + ON shop_category (is_active, sort_order, created_at DESC); + +CREATE TABLE IF NOT EXISTS shop_product +( + shop_product_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + shop_category_id uuid NOT NULL REFERENCES shop_category (shop_category_id), + slug text NOT NULL UNIQUE, + name text NOT NULL, + excerpt text, + description text, + seo_title text, + seo_description text, + og_title text, + og_description text, + indexable boolean NOT NULL DEFAULT true, + is_featured boolean NOT NULL DEFAULT false, + is_active boolean NOT NULL DEFAULT true, + sort_order integer NOT NULL DEFAULT 0, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS ix_shop_product_category_active_sort + ON shop_product (shop_category_id, is_active, sort_order, created_at DESC); + +CREATE INDEX IF NOT EXISTS ix_shop_product_featured_sort + ON shop_product (is_featured, is_active, sort_order, created_at DESC); + +CREATE TABLE IF NOT EXISTS shop_product_variant +( + shop_product_variant_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + shop_product_id uuid NOT NULL REFERENCES shop_product (shop_product_id) ON DELETE CASCADE, + sku text UNIQUE, + variant_label text NOT NULL, + color_name text NOT NULL, + color_hex text, + internal_material_code text NOT NULL, + price_chf numeric(12, 2) NOT NULL DEFAULT 0.00 CHECK (price_chf >= 0), + is_default boolean NOT NULL DEFAULT false, + is_active boolean NOT NULL DEFAULT true, + sort_order integer NOT NULL DEFAULT 0, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS ix_shop_product_variant_product_active_sort + ON shop_product_variant (shop_product_id, is_active, sort_order, created_at DESC); + +CREATE INDEX IF NOT EXISTS ix_shop_product_variant_sku + ON shop_product_variant (sku); + +CREATE TABLE IF NOT EXISTS shop_product_model_asset +( + shop_product_model_asset_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + shop_product_id uuid NOT NULL UNIQUE REFERENCES shop_product (shop_product_id) ON DELETE CASCADE, + original_filename text NOT NULL, + stored_relative_path text NOT NULL, + stored_filename text NOT NULL, + file_size_bytes bigint CHECK (file_size_bytes >= 0), + mime_type text, + sha256_hex text, + bounding_box_x_mm numeric(10, 3), + bounding_box_y_mm numeric(10, 3), + bounding_box_z_mm numeric(10, 3), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS ix_shop_product_model_asset_product + ON shop_product_model_asset (shop_product_id); + +ALTER TABLE quote_sessions + ADD COLUMN IF NOT EXISTS session_type text NOT NULL DEFAULT 'PRINT_QUOTE'; + +CREATE INDEX IF NOT EXISTS ix_quote_sessions_session_type + ON quote_sessions (session_type); + +ALTER TABLE quote_sessions + DROP CONSTRAINT IF EXISTS quote_sessions_session_type_check; + +ALTER TABLE quote_sessions + ADD CONSTRAINT quote_sessions_session_type_check + CHECK (session_type IN ('PRINT_QUOTE', 'SHOP_CART')); + +ALTER TABLE quote_line_items + ADD COLUMN IF NOT EXISTS line_item_type text NOT NULL DEFAULT 'PRINT_FILE'; + +ALTER TABLE quote_line_items + ADD COLUMN IF NOT EXISTS display_name text; + +ALTER TABLE quote_line_items + ADD COLUMN IF NOT EXISTS shop_product_id uuid; + +ALTER TABLE quote_line_items + ADD COLUMN IF NOT EXISTS shop_product_variant_id uuid; + +ALTER TABLE quote_line_items + ADD COLUMN IF NOT EXISTS shop_product_slug text; + +ALTER TABLE quote_line_items + ADD COLUMN IF NOT EXISTS shop_product_name text; + +ALTER TABLE quote_line_items + ADD COLUMN IF NOT EXISTS shop_variant_label text; + +ALTER TABLE quote_line_items + ADD COLUMN IF NOT EXISTS shop_variant_color_name text; + +ALTER TABLE quote_line_items + ADD COLUMN IF NOT EXISTS shop_variant_color_hex text; + +ALTER TABLE quote_line_items + ADD COLUMN IF NOT EXISTS stored_path text; + +CREATE INDEX IF NOT EXISTS ix_quote_line_items_shop_product + ON quote_line_items (shop_product_id); + +CREATE INDEX IF NOT EXISTS ix_quote_line_items_shop_product_variant + ON quote_line_items (shop_product_variant_id); + +ALTER TABLE quote_line_items + DROP CONSTRAINT IF EXISTS quote_line_items_line_item_type_check; + +ALTER TABLE quote_line_items + ADD CONSTRAINT quote_line_items_line_item_type_check + CHECK (line_item_type IN ('PRINT_FILE', 'SHOP_PRODUCT')); + +ALTER TABLE quote_line_items + DROP CONSTRAINT IF EXISTS fk_quote_line_items_shop_product; + +ALTER TABLE quote_line_items + ADD CONSTRAINT fk_quote_line_items_shop_product + FOREIGN KEY (shop_product_id) REFERENCES shop_product (shop_product_id); + +ALTER TABLE quote_line_items + DROP CONSTRAINT IF EXISTS fk_quote_line_items_shop_product_variant; + +ALTER TABLE quote_line_items + ADD CONSTRAINT fk_quote_line_items_shop_product_variant + FOREIGN KEY (shop_product_variant_id) REFERENCES shop_product_variant (shop_product_variant_id); + +ALTER TABLE orders + ADD COLUMN IF NOT EXISTS source_type text NOT NULL DEFAULT 'CALCULATOR'; + +CREATE INDEX IF NOT EXISTS ix_orders_source_type + ON orders (source_type); + +ALTER TABLE orders + DROP CONSTRAINT IF EXISTS orders_source_type_check; + +ALTER TABLE orders + ADD CONSTRAINT orders_source_type_check + CHECK (source_type IN ('CALCULATOR', 'SHOP')); + +ALTER TABLE order_items + ADD COLUMN IF NOT EXISTS item_type text NOT NULL DEFAULT 'PRINT_FILE'; + +ALTER TABLE order_items + ADD COLUMN IF NOT EXISTS display_name text; + +ALTER TABLE order_items + ADD COLUMN IF NOT EXISTS shop_product_id uuid; + +ALTER TABLE order_items + ADD COLUMN IF NOT EXISTS shop_product_variant_id uuid; + +ALTER TABLE order_items + ADD COLUMN IF NOT EXISTS shop_product_slug text; + +ALTER TABLE order_items + ADD COLUMN IF NOT EXISTS shop_product_name text; + +ALTER TABLE order_items + ADD COLUMN IF NOT EXISTS shop_variant_label text; + +ALTER TABLE order_items + ADD COLUMN IF NOT EXISTS shop_variant_color_name text; + +ALTER TABLE order_items + ADD COLUMN IF NOT EXISTS shop_variant_color_hex text; + +CREATE INDEX IF NOT EXISTS ix_order_items_shop_product + ON order_items (shop_product_id); + +CREATE INDEX IF NOT EXISTS ix_order_items_shop_product_variant + ON order_items (shop_product_variant_id); + +ALTER TABLE order_items + DROP CONSTRAINT IF EXISTS order_items_item_type_check; + +ALTER TABLE order_items + ADD CONSTRAINT order_items_item_type_check + CHECK (item_type IN ('PRINT_FILE', 'SHOP_PRODUCT')); + +ALTER TABLE order_items + DROP CONSTRAINT IF EXISTS fk_order_items_shop_product; + +ALTER TABLE order_items + ADD CONSTRAINT fk_order_items_shop_product + FOREIGN KEY (shop_product_id) REFERENCES shop_product (shop_product_id); + +ALTER TABLE order_items + DROP CONSTRAINT IF EXISTS fk_order_items_shop_product_variant; + +ALTER TABLE order_items + ADD CONSTRAINT fk_order_items_shop_product_variant + FOREIGN KEY (shop_product_variant_id) REFERENCES shop_product_variant (shop_product_variant_id); + ALTER TABLE quote_sessions DROP CONSTRAINT IF EXISTS fk_quote_sessions_source_request;