From fbed3f899f2bfba24d7b3a43210b1dcf24259821 Mon Sep 17 00:00:00 2001 From: Sebastian Frick Date: Wed, 18 Feb 2026 22:13:23 +0100 Subject: [PATCH] fix(masterdata): MASTERDATA-Permissions und JSON-Serialisierung der REST-Responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MASTERDATA_READ/WRITE fehlten im Permission-Enum und in den Rollen-Seed-Daten, dadurch bekam der Admin bei allen Stammdaten-Schreiboperationen Access Denied. Die Masterdata-Controller gaben Domain-Objekte direkt als JSON zurück, die von Jackson nicht serialisiert werden konnten (method-style Accessors statt JavaBean- Getter). Response-DTOs als Records eingeführt, die Domain-Objekte in flache JSON-Strukturen mappen. Frontend-Mapping-Layer entfernt, da Backend-Responses jetzt 1:1 die erwarteten Feldnamen liefern. --- .../domain/usermanagement/Permission.java | 4 + .../web/controller/ArticleController.java | 39 +++--- .../web/controller/CustomerController.java | 37 ++--- .../controller/ProductCategoryController.java | 16 +-- .../web/controller/SupplierController.java | 33 ++--- .../masterdata/web/dto/AddressResponse.java | 22 +++ .../masterdata/web/dto/ArticleResponse.java | 33 +++++ .../web/dto/ContactInfoResponse.java | 14 ++ .../web/dto/ContractLineItemResponse.java | 21 +++ .../masterdata/web/dto/CustomerResponse.java | 39 ++++++ .../web/dto/DeliveryAddressResponse.java | 19 +++ .../web/dto/FrameContractResponse.java | 25 ++++ .../web/dto/PaymentTermsResponse.java | 13 ++ .../web/dto/ProductCategoryResponse.java | 17 +++ .../web/dto/QualityCertificateResponse.java | 21 +++ .../masterdata/web/dto/SalesUnitResponse.java | 21 +++ .../web/dto/SupplierRatingResponse.java | 18 +++ .../masterdata/web/dto/SupplierResponse.java | 34 +++++ .../008-add-masterdata-permissions.sql | 30 ++++ .../008-add-masterdata-permissions.xml | 12 ++ .../db/changelog/db.changelog-master.xml | 1 + frontend/openapi.json | 2 +- .../api-client/src/resources/articles.ts | 93 +++---------- .../api-client/src/resources/categories.ts | 40 ++---- .../api-client/src/resources/customers.ts | 130 +++--------------- .../api-client/src/resources/suppliers.ts | 111 +++------------ 26 files changed, 481 insertions(+), 364 deletions(-) create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddressResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ArticleResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContactInfoResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContractLineItemResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CustomerResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/DeliveryAddressResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/FrameContractResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/PaymentTermsResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ProductCategoryResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/QualityCertificateResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SalesUnitResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierRatingResponse.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierResponse.java create mode 100644 backend/src/main/resources/db/changelog/changes/008-add-masterdata-permissions.sql create mode 100644 backend/src/main/resources/db/changelog/changes/008-add-masterdata-permissions.xml diff --git a/backend/src/main/java/de/effigenix/domain/usermanagement/Permission.java b/backend/src/main/java/de/effigenix/domain/usermanagement/Permission.java index 79ec86b..55e47fd 100644 --- a/backend/src/main/java/de/effigenix/domain/usermanagement/Permission.java +++ b/backend/src/main/java/de/effigenix/domain/usermanagement/Permission.java @@ -79,6 +79,10 @@ public enum Permission { LABEL_WRITE, LABEL_PRINT, + // ==================== Master Data BC ==================== + MASTERDATA_READ, + MASTERDATA_WRITE, + // ==================== Filiales BC ==================== BRANCH_READ, BRANCH_WRITE, diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ArticleController.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ArticleController.java index 5c89128..16c05ad 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ArticleController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ArticleController.java @@ -7,8 +7,6 @@ import de.effigenix.domain.masterdata.ArticleError; import de.effigenix.domain.masterdata.ArticleId; import de.effigenix.domain.masterdata.ArticleStatus; import de.effigenix.domain.masterdata.ProductCategoryId; -import de.effigenix.domain.masterdata.SalesUnitId; -import de.effigenix.domain.masterdata.SupplierId; import de.effigenix.infrastructure.masterdata.web.dto.*; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; @@ -73,7 +71,7 @@ public class ArticleController { @PostMapping @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity
createArticle( + public ResponseEntity createArticle( @Valid @RequestBody CreateArticleRequest request, Authentication authentication ) { @@ -91,11 +89,11 @@ public class ArticleController { } logger.info("Article created: {}", request.articleNumber()); - return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + return ResponseEntity.status(HttpStatus.CREATED).body(ArticleResponse.from(result.unsafeGetValue())); } @GetMapping - public ResponseEntity> listArticles( + public ResponseEntity> listArticles( @RequestParam(value = "categoryId", required = false) String categoryId, @RequestParam(value = "status", required = false) ArticleStatus status, Authentication authentication @@ -116,11 +114,12 @@ public class ArticleController { throw new ArticleDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(result.unsafeGetValue()); + var response = result.unsafeGetValue().stream().map(ArticleResponse::from).toList(); + return ResponseEntity.ok(response); } @GetMapping("/{id}") - public ResponseEntity
getArticle( + public ResponseEntity getArticle( @PathVariable("id") String articleId, Authentication authentication ) { @@ -133,12 +132,12 @@ public class ArticleController { throw new ArticleDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(ArticleResponse.from(result.unsafeGetValue())); } @PutMapping("/{id}") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity
updateArticle( + public ResponseEntity updateArticle( @PathVariable("id") String articleId, @Valid @RequestBody UpdateArticleRequest request, Authentication authentication @@ -154,12 +153,12 @@ public class ArticleController { } logger.info("Article updated: {}", articleId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(ArticleResponse.from(result.unsafeGetValue())); } @PostMapping("/{id}/activate") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity
activate( + public ResponseEntity activate( @PathVariable("id") String articleId, Authentication authentication ) { @@ -173,12 +172,12 @@ public class ArticleController { } logger.info("Article activated: {}", articleId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(ArticleResponse.from(result.unsafeGetValue())); } @PostMapping("/{id}/deactivate") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity
deactivate( + public ResponseEntity deactivate( @PathVariable("id") String articleId, Authentication authentication ) { @@ -192,12 +191,12 @@ public class ArticleController { } logger.info("Article deactivated: {}", articleId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(ArticleResponse.from(result.unsafeGetValue())); } @PostMapping("/{id}/sales-units") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity
addSalesUnit( + public ResponseEntity addSalesUnit( @PathVariable("id") String articleId, @Valid @RequestBody AddSalesUnitRequest request, Authentication authentication @@ -213,7 +212,7 @@ public class ArticleController { } logger.info("Sales unit added to article: {}", articleId); - return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + return ResponseEntity.status(HttpStatus.CREATED).body(ArticleResponse.from(result.unsafeGetValue())); } @DeleteMapping("/{id}/sales-units/{suId}") @@ -239,7 +238,7 @@ public class ArticleController { @PutMapping("/{id}/sales-units/{suId}/price") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity
updateSalesUnitPrice( + public ResponseEntity updateSalesUnitPrice( @PathVariable("id") String articleId, @PathVariable("suId") String salesUnitId, @Valid @RequestBody UpdateSalesUnitPriceRequest request, @@ -256,12 +255,12 @@ public class ArticleController { } logger.info("Sales unit price updated in article: {}", articleId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(ArticleResponse.from(result.unsafeGetValue())); } @PostMapping("/{id}/suppliers") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity
assignSupplier( + public ResponseEntity assignSupplier( @PathVariable("id") String articleId, @Valid @RequestBody AssignSupplierRequest request, Authentication authentication @@ -277,7 +276,7 @@ public class ArticleController { } logger.info("Supplier assigned to article: {}", articleId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(ArticleResponse.from(result.unsafeGetValue())); } @DeleteMapping("/{id}/suppliers/{supplierId}") diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/CustomerController.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/CustomerController.java index 0149033..945fd18 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/CustomerController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/CustomerController.java @@ -72,7 +72,7 @@ public class CustomerController { @PostMapping @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity createCustomer( + public ResponseEntity createCustomer( @Valid @RequestBody CreateCustomerRequest request, Authentication authentication ) { @@ -93,11 +93,11 @@ public class CustomerController { } logger.info("Customer created: {}", request.name()); - return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + return ResponseEntity.status(HttpStatus.CREATED).body(CustomerResponse.from(result.unsafeGetValue())); } @GetMapping - public ResponseEntity> listCustomers( + public ResponseEntity> listCustomers( @RequestParam(value = "type", required = false) CustomerType type, @RequestParam(value = "status", required = false) CustomerStatus status, Authentication authentication @@ -118,11 +118,12 @@ public class CustomerController { throw new CustomerDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(result.unsafeGetValue()); + var response = result.unsafeGetValue().stream().map(CustomerResponse::from).toList(); + return ResponseEntity.ok(response); } @GetMapping("/{id}") - public ResponseEntity getCustomer( + public ResponseEntity getCustomer( @PathVariable("id") String customerId, Authentication authentication ) { @@ -135,12 +136,12 @@ public class CustomerController { throw new CustomerDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(CustomerResponse.from(result.unsafeGetValue())); } @PutMapping("/{id}") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity updateCustomer( + public ResponseEntity updateCustomer( @PathVariable("id") String customerId, @Valid @RequestBody UpdateCustomerRequest request, Authentication authentication @@ -163,12 +164,12 @@ public class CustomerController { } logger.info("Customer updated: {}", customerId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(CustomerResponse.from(result.unsafeGetValue())); } @PostMapping("/{id}/activate") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity activate( + public ResponseEntity activate( @PathVariable("id") String customerId, Authentication authentication ) { @@ -182,12 +183,12 @@ public class CustomerController { } logger.info("Customer activated: {}", customerId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(CustomerResponse.from(result.unsafeGetValue())); } @PostMapping("/{id}/deactivate") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity deactivate( + public ResponseEntity deactivate( @PathVariable("id") String customerId, Authentication authentication ) { @@ -201,12 +202,12 @@ public class CustomerController { } logger.info("Customer deactivated: {}", customerId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(CustomerResponse.from(result.unsafeGetValue())); } @PostMapping("/{id}/delivery-addresses") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity addDeliveryAddress( + public ResponseEntity addDeliveryAddress( @PathVariable("id") String customerId, @Valid @RequestBody AddDeliveryAddressRequest request, Authentication authentication @@ -227,7 +228,7 @@ public class CustomerController { } logger.info("Delivery address added to customer: {}", customerId); - return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + return ResponseEntity.status(HttpStatus.CREATED).body(CustomerResponse.from(result.unsafeGetValue())); } @DeleteMapping("/{id}/delivery-addresses/{label}") @@ -253,7 +254,7 @@ public class CustomerController { @PutMapping("/{id}/frame-contract") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity setFrameContract( + public ResponseEntity setFrameContract( @PathVariable("id") String customerId, @Valid @RequestBody SetFrameContractRequest request, Authentication authentication @@ -277,7 +278,7 @@ public class CustomerController { } logger.info("Frame contract set for customer: {}", customerId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(CustomerResponse.from(result.unsafeGetValue())); } @DeleteMapping("/{id}/frame-contract") @@ -301,7 +302,7 @@ public class CustomerController { @PutMapping("/{id}/preferences") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity setPreferences( + public ResponseEntity setPreferences( @PathVariable("id") String customerId, @Valid @RequestBody SetPreferencesRequest request, Authentication authentication @@ -317,7 +318,7 @@ public class CustomerController { } logger.info("Preferences set for customer: {}", customerId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(CustomerResponse.from(result.unsafeGetValue())); } private ActorId extractActorId(Authentication authentication) { diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ProductCategoryController.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ProductCategoryController.java index 391a3bb..f8ca360 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ProductCategoryController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ProductCategoryController.java @@ -6,11 +6,10 @@ import de.effigenix.application.masterdata.ListProductCategories; import de.effigenix.application.masterdata.UpdateProductCategory; import de.effigenix.application.masterdata.command.CreateProductCategoryCommand; import de.effigenix.application.masterdata.command.UpdateProductCategoryCommand; -import de.effigenix.domain.masterdata.ProductCategory; import de.effigenix.domain.masterdata.ProductCategoryId; import de.effigenix.infrastructure.masterdata.web.dto.CreateProductCategoryRequest; +import de.effigenix.infrastructure.masterdata.web.dto.ProductCategoryResponse; import de.effigenix.infrastructure.masterdata.web.dto.UpdateProductCategoryRequest; -import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -52,7 +51,7 @@ public class ProductCategoryController { @PostMapping @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity createCategory( + public ResponseEntity createCategory( @Valid @RequestBody CreateProductCategoryRequest request, Authentication authentication ) { @@ -67,11 +66,11 @@ public class ProductCategoryController { } logger.info("Product category created: {}", request.name()); - return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + return ResponseEntity.status(HttpStatus.CREATED).body(ProductCategoryResponse.from(result.unsafeGetValue())); } @GetMapping - public ResponseEntity> listCategories(Authentication authentication) { + public ResponseEntity> listCategories(Authentication authentication) { var actorId = extractActorId(authentication); logger.info("Listing product categories by actor: {}", actorId.value()); @@ -81,12 +80,13 @@ public class ProductCategoryController { throw new ProductCategoryDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(result.unsafeGetValue()); + var response = result.unsafeGetValue().stream().map(ProductCategoryResponse::from).toList(); + return ResponseEntity.ok(response); } @PutMapping("/{id}") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity updateCategory( + public ResponseEntity updateCategory( @PathVariable("id") String categoryId, @Valid @RequestBody UpdateProductCategoryRequest request, Authentication authentication @@ -102,7 +102,7 @@ public class ProductCategoryController { } logger.info("Product category updated: {}", categoryId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(ProductCategoryResponse.from(result.unsafeGetValue())); } @DeleteMapping("/{id}") diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java index d0083d6..e93305a 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java @@ -64,7 +64,7 @@ public class SupplierController { @PostMapping @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity createSupplier( + public ResponseEntity createSupplier( @Valid @RequestBody CreateSupplierRequest request, Authentication authentication ) { @@ -84,11 +84,11 @@ public class SupplierController { } logger.info("Supplier created: {}", request.name()); - return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + return ResponseEntity.status(HttpStatus.CREATED).body(SupplierResponse.from(result.unsafeGetValue())); } @GetMapping - public ResponseEntity> listSuppliers( + public ResponseEntity> listSuppliers( @RequestParam(value = "status", required = false) SupplierStatus status, Authentication authentication ) { @@ -106,11 +106,12 @@ public class SupplierController { throw new SupplierDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(result.unsafeGetValue()); + var response = result.unsafeGetValue().stream().map(SupplierResponse::from).toList(); + return ResponseEntity.ok(response); } @GetMapping("/{id}") - public ResponseEntity getSupplier( + public ResponseEntity getSupplier( @PathVariable("id") String supplierId, Authentication authentication ) { @@ -123,12 +124,12 @@ public class SupplierController { throw new SupplierDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(SupplierResponse.from(result.unsafeGetValue())); } @PutMapping("/{id}") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity updateSupplier( + public ResponseEntity updateSupplier( @PathVariable("id") String supplierId, @Valid @RequestBody UpdateSupplierRequest request, Authentication authentication @@ -150,12 +151,12 @@ public class SupplierController { } logger.info("Supplier updated: {}", supplierId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(SupplierResponse.from(result.unsafeGetValue())); } @PostMapping("/{id}/activate") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity activate( + public ResponseEntity activate( @PathVariable("id") String supplierId, Authentication authentication ) { @@ -169,12 +170,12 @@ public class SupplierController { } logger.info("Supplier activated: {}", supplierId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(SupplierResponse.from(result.unsafeGetValue())); } @PostMapping("/{id}/deactivate") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity deactivate( + public ResponseEntity deactivate( @PathVariable("id") String supplierId, Authentication authentication ) { @@ -188,12 +189,12 @@ public class SupplierController { } logger.info("Supplier deactivated: {}", supplierId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(SupplierResponse.from(result.unsafeGetValue())); } @PostMapping("/{id}/rating") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity rateSupplier( + public ResponseEntity rateSupplier( @PathVariable("id") String supplierId, @Valid @RequestBody RateSupplierRequest request, Authentication authentication @@ -211,12 +212,12 @@ public class SupplierController { } logger.info("Supplier rated: {}", supplierId); - return ResponseEntity.ok(result.unsafeGetValue()); + return ResponseEntity.ok(SupplierResponse.from(result.unsafeGetValue())); } @PostMapping("/{id}/certificates") @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") - public ResponseEntity addCertificate( + public ResponseEntity addCertificate( @PathVariable("id") String supplierId, @Valid @RequestBody AddCertificateRequest request, Authentication authentication @@ -235,7 +236,7 @@ public class SupplierController { } logger.info("Certificate added to supplier: {}", supplierId); - return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + return ResponseEntity.status(HttpStatus.CREATED).body(SupplierResponse.from(result.unsafeGetValue())); } @DeleteMapping("/{id}/certificates") diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddressResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddressResponse.java new file mode 100644 index 0000000..aa4790a --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddressResponse.java @@ -0,0 +1,22 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.shared.common.Address; + +public record AddressResponse( + String street, + String houseNumber, + String postalCode, + String city, + String country +) { + public static AddressResponse from(Address address) { + if (address == null) return null; + return new AddressResponse( + address.street(), + address.houseNumber(), + address.postalCode(), + address.city(), + address.country() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ArticleResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ArticleResponse.java new file mode 100644 index 0000000..bd5ecaa --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ArticleResponse.java @@ -0,0 +1,33 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.Article; +import de.effigenix.domain.masterdata.SupplierId; + +import java.time.LocalDateTime; +import java.util.List; + +public record ArticleResponse( + String id, + String name, + String articleNumber, + String categoryId, + List salesUnits, + String status, + List supplierIds, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public static ArticleResponse from(Article article) { + return new ArticleResponse( + article.id().value(), + article.name().value(), + article.articleNumber().value(), + article.categoryId().value(), + article.salesUnits().stream().map(SalesUnitResponse::from).toList(), + article.status().name(), + article.supplierReferences().stream().map(SupplierId::value).toList(), + article.createdAt(), + article.updatedAt() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContactInfoResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContactInfoResponse.java new file mode 100644 index 0000000..240ecb2 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContactInfoResponse.java @@ -0,0 +1,14 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.shared.common.ContactInfo; + +public record ContactInfoResponse( + String phone, + String email, + String contactPerson +) { + public static ContactInfoResponse from(ContactInfo ci) { + if (ci == null) return null; + return new ContactInfoResponse(ci.phone(), ci.email(), ci.contactPerson()); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContractLineItemResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContractLineItemResponse.java new file mode 100644 index 0000000..e533d29 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ContractLineItemResponse.java @@ -0,0 +1,21 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.ContractLineItem; + +import java.math.BigDecimal; + +public record ContractLineItemResponse( + String articleId, + BigDecimal agreedPrice, + BigDecimal agreedQuantity, + String unit +) { + public static ContractLineItemResponse from(ContractLineItem item) { + return new ContractLineItemResponse( + item.articleId().value(), + item.agreedPrice().amount(), + item.agreedQuantity(), + item.unit().name() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CustomerResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CustomerResponse.java new file mode 100644 index 0000000..8073d23 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CustomerResponse.java @@ -0,0 +1,39 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.Customer; +import de.effigenix.domain.masterdata.CustomerPreference; + +import java.time.LocalDateTime; +import java.util.List; + +public record CustomerResponse( + String id, + String name, + String type, + AddressResponse billingAddress, + ContactInfoResponse contactInfo, + PaymentTermsResponse paymentTerms, + List deliveryAddresses, + FrameContractResponse frameContract, + List preferences, + String status, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public static CustomerResponse from(Customer customer) { + return new CustomerResponse( + customer.id().value(), + customer.name().value(), + customer.type().name(), + AddressResponse.from(customer.billingAddress()), + ContactInfoResponse.from(customer.contactInfo()), + PaymentTermsResponse.from(customer.paymentTerms()), + customer.deliveryAddresses().stream().map(DeliveryAddressResponse::from).toList(), + FrameContractResponse.from(customer.frameContract()), + customer.preferences().stream().map(CustomerPreference::name).toList(), + customer.status().name(), + customer.createdAt(), + customer.updatedAt() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/DeliveryAddressResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/DeliveryAddressResponse.java new file mode 100644 index 0000000..e4bc037 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/DeliveryAddressResponse.java @@ -0,0 +1,19 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.DeliveryAddress; + +public record DeliveryAddressResponse( + String label, + AddressResponse address, + String contactPerson, + String deliveryNotes +) { + public static DeliveryAddressResponse from(DeliveryAddress da) { + return new DeliveryAddressResponse( + da.label(), + AddressResponse.from(da.address()), + da.contactPerson(), + da.deliveryNotes() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/FrameContractResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/FrameContractResponse.java new file mode 100644 index 0000000..8294c3a --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/FrameContractResponse.java @@ -0,0 +1,25 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.FrameContract; + +import java.time.LocalDate; +import java.util.List; + +public record FrameContractResponse( + String id, + LocalDate validFrom, + LocalDate validUntil, + String deliveryRhythm, + List lineItems +) { + public static FrameContractResponse from(FrameContract fc) { + if (fc == null) return null; + return new FrameContractResponse( + fc.id().value(), + fc.validFrom(), + fc.validUntil(), + fc.deliveryRhythm().name(), + fc.lineItems().stream().map(ContractLineItemResponse::from).toList() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/PaymentTermsResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/PaymentTermsResponse.java new file mode 100644 index 0000000..148324b --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/PaymentTermsResponse.java @@ -0,0 +1,13 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.shared.common.PaymentTerms; + +public record PaymentTermsResponse( + int paymentDueDays, + String paymentDescription +) { + public static PaymentTermsResponse from(PaymentTerms pt) { + if (pt == null) return null; + return new PaymentTermsResponse(pt.paymentDueDays(), pt.description()); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ProductCategoryResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ProductCategoryResponse.java new file mode 100644 index 0000000..0fddd36 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/ProductCategoryResponse.java @@ -0,0 +1,17 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.ProductCategory; + +public record ProductCategoryResponse( + String id, + String name, + String description +) { + public static ProductCategoryResponse from(ProductCategory category) { + return new ProductCategoryResponse( + category.id().value(), + category.name().value(), + category.description() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/QualityCertificateResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/QualityCertificateResponse.java new file mode 100644 index 0000000..014ff99 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/QualityCertificateResponse.java @@ -0,0 +1,21 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.QualityCertificate; + +import java.time.LocalDate; + +public record QualityCertificateResponse( + String certificateType, + String issuer, + LocalDate validFrom, + LocalDate validUntil +) { + public static QualityCertificateResponse from(QualityCertificate cert) { + return new QualityCertificateResponse( + cert.certificateType(), + cert.issuer(), + cert.validFrom(), + cert.validUntil() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SalesUnitResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SalesUnitResponse.java new file mode 100644 index 0000000..0672866 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SalesUnitResponse.java @@ -0,0 +1,21 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.SalesUnit; + +import java.math.BigDecimal; + +public record SalesUnitResponse( + String id, + String unit, + String priceModel, + BigDecimal price +) { + public static SalesUnitResponse from(SalesUnit su) { + return new SalesUnitResponse( + su.id().value(), + su.unit().name(), + su.priceModel().name(), + su.price().amount() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierRatingResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierRatingResponse.java new file mode 100644 index 0000000..e0e92d1 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierRatingResponse.java @@ -0,0 +1,18 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.SupplierRating; + +public record SupplierRatingResponse( + int qualityScore, + int deliveryScore, + int priceScore +) { + public static SupplierRatingResponse from(SupplierRating rating) { + if (rating == null) return null; + return new SupplierRatingResponse( + rating.qualityScore(), + rating.deliveryScore(), + rating.priceScore() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierResponse.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierResponse.java new file mode 100644 index 0000000..5f99354 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/SupplierResponse.java @@ -0,0 +1,34 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.Supplier; + +import java.time.LocalDateTime; +import java.util.List; + +public record SupplierResponse( + String id, + String name, + AddressResponse address, + ContactInfoResponse contactInfo, + PaymentTermsResponse paymentTerms, + List certificates, + SupplierRatingResponse rating, + String status, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public static SupplierResponse from(Supplier supplier) { + return new SupplierResponse( + supplier.id().value(), + supplier.name().value(), + AddressResponse.from(supplier.address()), + ContactInfoResponse.from(supplier.contactInfo()), + PaymentTermsResponse.from(supplier.paymentTerms()), + supplier.certificates().stream().map(QualityCertificateResponse::from).toList(), + SupplierRatingResponse.from(supplier.rating()), + supplier.status().name(), + supplier.createdAt(), + supplier.updatedAt() + ); + } +} diff --git a/backend/src/main/resources/db/changelog/changes/008-add-masterdata-permissions.sql b/backend/src/main/resources/db/changelog/changes/008-add-masterdata-permissions.sql new file mode 100644 index 0000000..510c067 --- /dev/null +++ b/backend/src/main/resources/db/changelog/changes/008-add-masterdata-permissions.sql @@ -0,0 +1,30 @@ +-- Add MASTERDATA_READ and MASTERDATA_WRITE permissions to relevant roles. +-- These permissions are required by the Masterdata BC controllers +-- (ArticleController, ProductCategoryController, SupplierController, CustomerController). + +-- ADMIN gets both READ and WRITE +INSERT INTO role_permissions (role_id, permission) VALUES + ('c0a80121-0000-0000-0000-000000000001', 'MASTERDATA_READ'), + ('c0a80121-0000-0000-0000-000000000001', 'MASTERDATA_WRITE'); + +-- PROCUREMENT_MANAGER gets both (manages suppliers, articles) +INSERT INTO role_permissions (role_id, permission) VALUES + ('c0a80121-0000-0000-0000-000000000006', 'MASTERDATA_READ'), + ('c0a80121-0000-0000-0000-000000000006', 'MASTERDATA_WRITE'); + +-- SALES_MANAGER gets both (manages customers) +INSERT INTO role_permissions (role_id, permission) VALUES + ('c0a80121-0000-0000-0000-000000000008', 'MASTERDATA_READ'), + ('c0a80121-0000-0000-0000-000000000008', 'MASTERDATA_WRITE'); + +-- PRODUCTION_MANAGER gets READ (needs to view articles, categories) +INSERT INTO role_permissions (role_id, permission) VALUES + ('c0a80121-0000-0000-0000-000000000002', 'MASTERDATA_READ'); + +-- WAREHOUSE_WORKER gets READ (needs to view articles) +INSERT INTO role_permissions (role_id, permission) VALUES + ('c0a80121-0000-0000-0000-000000000007', 'MASTERDATA_READ'); + +-- SALES_STAFF gets READ (needs to view articles, customers) +INSERT INTO role_permissions (role_id, permission) VALUES + ('c0a80121-0000-0000-0000-000000000009', 'MASTERDATA_READ'); diff --git a/backend/src/main/resources/db/changelog/changes/008-add-masterdata-permissions.xml b/backend/src/main/resources/db/changelog/changes/008-add-masterdata-permissions.xml new file mode 100644 index 0000000..93db118 --- /dev/null +++ b/backend/src/main/resources/db/changelog/changes/008-add-masterdata-permissions.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/backend/src/main/resources/db/changelog/db.changelog-master.xml b/backend/src/main/resources/db/changelog/db.changelog-master.xml index a1d6cec..e28af5a 100644 --- a/backend/src/main/resources/db/changelog/db.changelog-master.xml +++ b/backend/src/main/resources/db/changelog/db.changelog-master.xml @@ -12,5 +12,6 @@ + diff --git a/frontend/openapi.json b/frontend/openapi.json index 491701e..9a70da4 100644 --- a/frontend/openapi.json +++ b/frontend/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.1","info":{"title":"Effigenix Fleischerei ERP API","description":"RESTful API for Effigenix Fleischerei ERP System.\n\n## Authentication\n\nAll endpoints (except /api/auth/login and /api/auth/refresh) require JWT authentication.\n\n1. Login via POST /api/auth/login with username and password\n2. Copy the returned access token\n3. Click \"Authorize\" button (top right)\n4. Enter: Bearer \n5. Click \"Authorize\"\n\n## User Management\n\n- **Authentication**: Login, logout, refresh token\n- **User Management**: Create, update, list users (ADMIN only)\n- **Role Management**: Assign roles, lock/unlock users (ADMIN only)\n- **Password Management**: Change password (requires current password)\n\n## Error Handling\n\nAll errors return a consistent error response format:\n\n```json\n{\n \"code\": \"USER_NOT_FOUND\",\n \"message\": \"User with ID 'user-123' not found\",\n \"status\": 404,\n \"timestamp\": \"2026-02-17T12:00:00\",\n \"path\": \"/api/users/user-123\",\n \"validationErrors\": null\n}\n```\n\n## Architecture\n\nBuilt with:\n- Domain-Driven Design (DDD)\n- Clean Architecture (Hexagonal Architecture)\n- Spring Boot 3.2\n- Java 21\n- PostgreSQL\n","contact":{"name":"Effigenix Development Team","url":"https://effigenix.com","email":"dev@effigenix.com"},"license":{"name":"Proprietary","url":"https://effigenix.com/license"},"version":"0.1.0"},"servers":[{"url":"http://localhost:8080","description":"Local Development Server"},{"url":"https://api.effigenix.com","description":"Production Server"}],"tags":[{"name":"Product Categories","description":"Product category management endpoints"},{"name":"User Management","description":"User management endpoints (requires authentication)"},{"name":"Articles","description":"Article management endpoints"},{"name":"Role Management","description":"Role management endpoints (ADMIN only)"},{"name":"Customers","description":"Customer management endpoints"},{"name":"Suppliers","description":"Supplier management endpoints"},{"name":"Authentication","description":"Authentication and session management endpoints"}],"paths":{"/api/users/{id}":{"get":{"tags":["User Management"],"summary":"Get user by ID","description":"Retrieve a single user by their ID. Requires authentication.","operationId":"getUserById","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"responses":{"200":{"description":"User retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["User Management"],"summary":"Update user","description":"Update user details (email, branchId). Only provided fields will be updated.","operationId":"updateUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"409":{"description":"Email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User updated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/password":{"put":{"tags":["User Management"],"summary":"Change password","description":"Change user password. Requires current password for verification.","operationId":"changePassword","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"400":{"description":"Invalid password"},"401":{"description":"Invalid current password or authentication required"},"404":{"description":"User not found"},"204":{"description":"Password changed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}":{"get":{"tags":["Suppliers"],"operationId":"getSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Supplier"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Suppliers"],"operationId":"updateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Supplier"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}":{"get":{"tags":["Customers"],"operationId":"getCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Customer"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Customers"],"operationId":"updateCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Customer"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/preferences":{"put":{"tags":["Customers"],"operationId":"setPreferences","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPreferencesRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Customer"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/frame-contract":{"put":{"tags":["Customers"],"operationId":"setFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetFrameContractRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Customer"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Customers"],"operationId":"removeFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/categories/{id}":{"put":{"tags":["Product Categories"],"operationId":"updateCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategory"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Product Categories"],"operationId":"deleteCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}":{"get":{"tags":["Articles"],"operationId":"getArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Article"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Articles"],"operationId":"updateArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Article"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}/price":{"put":{"tags":["Articles"],"operationId":"updateSalesUnitPrice","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSalesUnitPriceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Article"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users":{"get":{"tags":["User Management"],"summary":"List all users","description":"Get a list of all users in the system. Requires authentication.","operationId":"listUsers","responses":{"200":{"description":"Users retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["User Management"],"summary":"Create user (ADMIN only)","description":"Create a new user account with specified roles. Requires USER_MANAGEMENT permission.","operationId":"createUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"400":{"description":"Validation error or invalid password","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"201":{"description":"User created successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Username or email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/unlock":{"post":{"tags":["User Management"],"summary":"Unlock user (ADMIN only)","description":"Unlock a user account (allows login). Requires USER_MANAGEMENT permission.","operationId":"unlockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"responses":{"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User unlocked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles":{"post":{"tags":["User Management"],"summary":"Assign role (ADMIN only)","description":"Assign a role to a user. Requires USER_MANAGEMENT permission.","operationId":"assignRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"404":{"description":"User or role not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"Role assigned successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/lock":{"post":{"tags":["User Management"],"summary":"Lock user (ADMIN only)","description":"Lock a user account (prevents login). Requires USER_MANAGEMENT permission.","operationId":"lockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"responses":{"200":{"description":"User locked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers":{"get":{"tags":["Suppliers"],"operationId":"listSuppliers","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Supplier"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Suppliers"],"operationId":"createSupplier","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Supplier"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/rating":{"post":{"tags":["Suppliers"],"operationId":"rateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Supplier"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/deactivate":{"post":{"tags":["Suppliers"],"operationId":"deactivate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Supplier"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/certificates":{"post":{"tags":["Suppliers"],"operationId":"addCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Supplier"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Suppliers"],"operationId":"removeCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/activate":{"post":{"tags":["Suppliers"],"operationId":"activate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Supplier"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers":{"get":{"tags":["Customers"],"operationId":"listCustomers","parameters":[{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["B2C","B2B"]}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Customers"],"operationId":"createCustomer","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Customer"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses":{"post":{"tags":["Customers"],"operationId":"addDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddDeliveryAddressRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Customer"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/deactivate":{"post":{"tags":["Customers"],"operationId":"deactivate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Customer"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/activate":{"post":{"tags":["Customers"],"operationId":"activate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Customer"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/categories":{"get":{"tags":["Product Categories"],"operationId":"listCategories","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProductCategory"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Product Categories"],"operationId":"createCategory","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategory"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Refresh access token","description":"Refresh an expired access token using a valid refresh token. Returns new access token and refresh token.","operationId":"refresh","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"401":{"description":"Invalid or expired refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"200":{"description":"Token refresh successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"description":"Validation error (missing refresh token)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/auth/logout":{"post":{"tags":["Authentication"],"summary":"User logout","description":"Invalidate current JWT token. Requires authentication.","operationId":"logout","responses":{"401":{"description":"Invalid or missing authentication token"},"204":{"description":"Logout successful"}}}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"User login","description":"Authenticate user with username and password. Returns JWT access token and refresh token.","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Login successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid credentials, user locked, or user inactive","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"description":"Validation error (missing username or password)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/articles":{"get":{"tags":["Articles"],"operationId":"listArticles","parameters":[{"name":"categoryId","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Article"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Articles"],"operationId":"createArticle","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Article"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers":{"post":{"tags":["Articles"],"operationId":"assignSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Article"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units":{"post":{"tags":["Articles"],"operationId":"addSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSalesUnitRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Article"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/deactivate":{"post":{"tags":["Articles"],"operationId":"deactivate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Article"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/activate":{"post":{"tags":["Articles"],"operationId":"activate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Article"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/roles":{"get":{"tags":["Role Management"],"summary":"List all roles (ADMIN only)","description":"Get a list of all available roles in the system. Requires USER_MANAGEMENT permission.","operationId":"listRoles","responses":{"200":{"description":"Roles retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RoleDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles/{roleName}":{"delete":{"tags":["User Management"],"summary":"Remove role (ADMIN only)","description":"Remove a role from a user. Requires USER_MANAGEMENT permission.","operationId":"removeRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"},{"name":"roleName","in":"path","description":"Role name","required":true,"schema":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]},"example":"MANAGER"}],"responses":{"404":{"description":"User or role not found"},"403":{"description":"Missing USER_MANAGEMENT permission"},"204":{"description":"Role removed successfully"},"401":{"description":"Authentication required"}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses/{label}":{"delete":{"tags":["Customers"],"operationId":"removeDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"label","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers/{supplierId}":{"delete":{"tags":["Articles"],"operationId":"removeSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"supplierId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}":{"delete":{"tags":["Articles"],"operationId":"removeSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}}},"components":{"schemas":{"UpdateUserRequest":{"type":"object","properties":{"email":{"type":"string","description":"New email address","example":"newemail@example.com"},"branchId":{"type":"string","description":"New branch ID","example":"BRANCH-002"}},"description":"Request to update user details"},"RoleDTO":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]},"permissions":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["RECIPE_READ","RECIPE_WRITE","RECIPE_DELETE","BATCH_READ","BATCH_WRITE","BATCH_COMPLETE","BATCH_DELETE","PRODUCTION_ORDER_READ","PRODUCTION_ORDER_WRITE","PRODUCTION_ORDER_DELETE","HACCP_READ","HACCP_WRITE","TEMPERATURE_LOG_READ","TEMPERATURE_LOG_WRITE","CLEANING_RECORD_READ","CLEANING_RECORD_WRITE","GOODS_INSPECTION_READ","GOODS_INSPECTION_WRITE","STOCK_READ","STOCK_WRITE","STOCK_MOVEMENT_READ","STOCK_MOVEMENT_WRITE","INVENTORY_COUNT_READ","INVENTORY_COUNT_WRITE","PURCHASE_ORDER_READ","PURCHASE_ORDER_WRITE","PURCHASE_ORDER_DELETE","GOODS_RECEIPT_READ","GOODS_RECEIPT_WRITE","SUPPLIER_READ","SUPPLIER_WRITE","SUPPLIER_DELETE","ORDER_READ","ORDER_WRITE","ORDER_DELETE","INVOICE_READ","INVOICE_WRITE","INVOICE_DELETE","CUSTOMER_READ","CUSTOMER_WRITE","CUSTOMER_DELETE","LABEL_READ","LABEL_WRITE","LABEL_PRINT","BRANCH_READ","BRANCH_WRITE","BRANCH_DELETE","USER_READ","USER_WRITE","USER_DELETE","USER_LOCK","USER_UNLOCK","ROLE_READ","ROLE_WRITE","ROLE_ASSIGN","ROLE_REMOVE","REPORT_READ","REPORT_GENERATE","NOTIFICATION_READ","NOTIFICATION_SEND","AUDIT_LOG_READ","SYSTEM_SETTINGS_READ","SYSTEM_SETTINGS_WRITE"]}},"description":{"type":"string"}}},"UserDTO":{"type":"object","properties":{"id":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"roles":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}},"branchId":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","LOCKED"]},"createdAt":{"type":"string","format":"date-time"},"lastLogin":{"type":"string","format":"date-time"}}},"ChangePasswordRequest":{"required":["currentPassword","newPassword"],"type":"object","properties":{"currentPassword":{"type":"string","description":"Current password","example":"OldPass123"},"newPassword":{"maxLength":2147483647,"minLength":8,"type":"string","description":"New password (min 8 characters)","example":"NewSecurePass456"}},"description":"Request to change user password"},"UpdateSupplierRequest":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"Supplier":{"type":"object"},"UpdateCustomerRequest":{"type":"object","properties":{"name":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"Customer":{"type":"object","properties":{"frameContract":{"$ref":"#/components/schemas/ResultCustomerErrorVoid"},"preferences":{"uniqueItems":true,"type":"array","writeOnly":true,"items":{"type":"string","enum":["BIO","REGIONAL","TIERWOHL","HALAL","KOSHER","GLUTENFREI","LAKTOSEFREI"]}}}},"ResultCustomerErrorVoid":{"type":"object","properties":{"failure":{"type":"boolean"},"success":{"type":"boolean"}}},"SetPreferencesRequest":{"required":["preferences"],"type":"object","properties":{"preferences":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["BIO","REGIONAL","TIERWOHL","HALAL","KOSHER","GLUTENFREI","LAKTOSEFREI"]}}}},"LineItem":{"required":["agreedPrice","articleId"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]}}},"SetFrameContractRequest":{"required":["lineItems","rhythm"],"type":"object","properties":{"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"rhythm":{"type":"string","enum":["DAILY","WEEKLY","BIWEEKLY","MONTHLY","ON_DEMAND"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/LineItem"}}}},"UpdateProductCategoryRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ProductCategory":{"type":"object"},"UpdateArticleRequest":{"type":"object","properties":{"name":{"type":"string"},"categoryId":{"type":"string"}}},"Article":{"type":"object"},"UpdateSalesUnitPriceRequest":{"required":["price"],"type":"object","properties":{"price":{"type":"number"}}},"CreateUserRequest":{"required":["email","password","roleNames","username"],"type":"object","properties":{"username":{"maxLength":50,"minLength":3,"type":"string","description":"Username (unique)","example":"john.doe"},"email":{"type":"string","description":"Email address (unique)","example":"john.doe@example.com"},"password":{"maxLength":2147483647,"minLength":8,"type":"string","description":"Password (min 8 characters)","example":"SecurePass123"},"roleNames":{"uniqueItems":true,"type":"array","description":"Role names to assign","example":["USER","MANAGER"],"items":{"type":"string","description":"Role names to assign","example":"[\"USER\",\"MANAGER\"]","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"branchId":{"type":"string","description":"Branch ID (optional)","example":"BRANCH-001"}},"description":"Request to create a new user"},"AssignRoleRequest":{"required":["roleName"],"type":"object","properties":{"roleName":{"type":"string","description":"Role name to assign","example":"MANAGER","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"description":"Request to assign a role to a user"},"CreateSupplierRequest":{"required":["name","phone"],"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"RateSupplierRequest":{"type":"object","properties":{"qualityScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"deliveryScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"priceScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"}}},"AddCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"CreateCustomerRequest":{"required":["city","country","name","phone","postalCode","street","type"],"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["B2C","B2B"]},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddDeliveryAddressRequest":{"required":["city","country","postalCode","street"],"type":"object","properties":{"label":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"CreateProductCategoryRequest":{"required":["name"],"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"RefreshTokenRequest":{"required":["refreshToken"],"type":"object","properties":{"refreshToken":{"type":"string","description":"Refresh token"}},"description":"Refresh token request"},"LoginResponse":{"type":"object","properties":{"accessToken":{"type":"string","description":"JWT access token","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"tokenType":{"type":"string","description":"Token type","example":"Bearer"},"expiresIn":{"type":"integer","description":"Token expiration time in seconds","format":"int64","example":3600},"expiresAt":{"type":"string","description":"Token expiration timestamp","format":"date-time"},"refreshToken":{"type":"string","description":"Refresh token for obtaining new access token"}},"description":"Login response with JWT tokens"},"LoginRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string","description":"Username","example":"admin"},"password":{"type":"string","description":"Password","example":"admin123"}},"description":"Login request with username and password"},"CreateArticleRequest":{"required":["articleNumber","categoryId","name","price","priceModel","unit"],"type":"object","properties":{"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"AssignSupplierRequest":{"required":["supplierId"],"type":"object","properties":{"supplierId":{"type":"string"}}},"AddSalesUnitRequest":{"required":["price","priceModel","unit"],"type":"object","properties":{"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"RemoveCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"}}}},"securitySchemes":{"Bearer Authentication":{"type":"http","description":"JWT authentication token obtained from POST /api/auth/login.\n\nFormat: Bearer \n\nExample:\nBearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\n","scheme":"bearer","bearerFormat":"JWT"}}}} \ No newline at end of file +{"openapi":"3.0.1","info":{"title":"Effigenix Fleischerei ERP API","description":"RESTful API for Effigenix Fleischerei ERP System.\n\n## Authentication\n\nAll endpoints (except /api/auth/login and /api/auth/refresh) require JWT authentication.\n\n1. Login via POST /api/auth/login with username and password\n2. Copy the returned access token\n3. Click \"Authorize\" button (top right)\n4. Enter: Bearer \n5. Click \"Authorize\"\n\n## User Management\n\n- **Authentication**: Login, logout, refresh token\n- **User Management**: Create, update, list users (ADMIN only)\n- **Role Management**: Assign roles, lock/unlock users (ADMIN only)\n- **Password Management**: Change password (requires current password)\n\n## Error Handling\n\nAll errors return a consistent error response format:\n\n```json\n{\n \"code\": \"USER_NOT_FOUND\",\n \"message\": \"User with ID 'user-123' not found\",\n \"status\": 404,\n \"timestamp\": \"2026-02-17T12:00:00\",\n \"path\": \"/api/users/user-123\",\n \"validationErrors\": null\n}\n```\n\n## Architecture\n\nBuilt with:\n- Domain-Driven Design (DDD)\n- Clean Architecture (Hexagonal Architecture)\n- Spring Boot 3.2\n- Java 21\n- PostgreSQL\n","contact":{"name":"Effigenix Development Team","url":"https://effigenix.com","email":"dev@effigenix.com"},"license":{"name":"Proprietary","url":"https://effigenix.com/license"},"version":"0.1.0"},"servers":[{"url":"http://localhost:8080","description":"Local Development Server"},{"url":"https://api.effigenix.com","description":"Production Server"}],"tags":[{"name":"Product Categories","description":"Product category management endpoints"},{"name":"User Management","description":"User management endpoints (requires authentication)"},{"name":"Articles","description":"Article management endpoints"},{"name":"Role Management","description":"Role management endpoints (ADMIN only)"},{"name":"Customers","description":"Customer management endpoints"},{"name":"Suppliers","description":"Supplier management endpoints"},{"name":"Authentication","description":"Authentication and session management endpoints"}],"paths":{"/api/users/{id}":{"get":{"tags":["User Management"],"summary":"Get user by ID","description":"Retrieve a single user by their ID. Requires authentication.","operationId":"getUserById","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"responses":{"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["User Management"],"summary":"Update user","description":"Update user details (email, branchId). Only provided fields will be updated.","operationId":"updateUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"409":{"description":"Email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User updated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/password":{"put":{"tags":["User Management"],"summary":"Change password","description":"Change user password. Requires current password for verification.","operationId":"changePassword","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"400":{"description":"Invalid password"},"401":{"description":"Invalid current password or authentication required"},"404":{"description":"User not found"},"204":{"description":"Password changed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}":{"get":{"tags":["Suppliers"],"operationId":"getSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Suppliers"],"operationId":"updateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}":{"get":{"tags":["Customers"],"operationId":"getCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Customers"],"operationId":"updateCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/preferences":{"put":{"tags":["Customers"],"operationId":"setPreferences","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPreferencesRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/frame-contract":{"put":{"tags":["Customers"],"operationId":"setFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetFrameContractRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Customers"],"operationId":"removeFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/categories/{id}":{"put":{"tags":["Product Categories"],"operationId":"updateCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Product Categories"],"operationId":"deleteCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}":{"get":{"tags":["Articles"],"operationId":"getArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Articles"],"operationId":"updateArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}/price":{"put":{"tags":["Articles"],"operationId":"updateSalesUnitPrice","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSalesUnitPriceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users":{"get":{"tags":["User Management"],"summary":"List all users","description":"Get a list of all users in the system. Requires authentication.","operationId":"listUsers","responses":{"200":{"description":"Users retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["User Management"],"summary":"Create user (ADMIN only)","description":"Create a new user account with specified roles. Requires USER_MANAGEMENT permission.","operationId":"createUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"201":{"description":"User created successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"400":{"description":"Validation error or invalid password","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Username or email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/unlock":{"post":{"tags":["User Management"],"summary":"Unlock user (ADMIN only)","description":"Unlock a user account (allows login). Requires USER_MANAGEMENT permission.","operationId":"unlockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"responses":{"200":{"description":"User unlocked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles":{"post":{"tags":["User Management"],"summary":"Assign role (ADMIN only)","description":"Assign a role to a user. Requires USER_MANAGEMENT permission.","operationId":"assignRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"200":{"description":"Role assigned successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User or role not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/lock":{"post":{"tags":["User Management"],"summary":"Lock user (ADMIN only)","description":"Lock a user account (prevents login). Requires USER_MANAGEMENT permission.","operationId":"lockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"}],"responses":{"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User locked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers":{"get":{"tags":["Suppliers"],"operationId":"listSuppliers","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SupplierResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Suppliers"],"operationId":"createSupplier","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/rating":{"post":{"tags":["Suppliers"],"operationId":"rateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/deactivate":{"post":{"tags":["Suppliers"],"operationId":"deactivate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/certificates":{"post":{"tags":["Suppliers"],"operationId":"addCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Suppliers"],"operationId":"removeCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/activate":{"post":{"tags":["Suppliers"],"operationId":"activate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers":{"get":{"tags":["Customers"],"operationId":"listCustomers","parameters":[{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["B2C","B2B"]}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CustomerResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Customers"],"operationId":"createCustomer","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses":{"post":{"tags":["Customers"],"operationId":"addDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddDeliveryAddressRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/deactivate":{"post":{"tags":["Customers"],"operationId":"deactivate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/activate":{"post":{"tags":["Customers"],"operationId":"activate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/categories":{"get":{"tags":["Product Categories"],"operationId":"listCategories","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Product Categories"],"operationId":"createCategory","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Refresh access token","description":"Refresh an expired access token using a valid refresh token. Returns new access token and refresh token.","operationId":"refresh","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"200":{"description":"Token refresh successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid or expired refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"description":"Validation error (missing refresh token)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/auth/logout":{"post":{"tags":["Authentication"],"summary":"User logout","description":"Invalidate current JWT token. Requires authentication.","operationId":"logout","responses":{"401":{"description":"Invalid or missing authentication token"},"204":{"description":"Logout successful"}}}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"User login","description":"Authenticate user with username and password. Returns JWT access token and refresh token.","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Login successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid credentials, user locked, or user inactive","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"description":"Validation error (missing username or password)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/articles":{"get":{"tags":["Articles"],"operationId":"listArticles","parameters":[{"name":"categoryId","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ArticleResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Articles"],"operationId":"createArticle","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers":{"post":{"tags":["Articles"],"operationId":"assignSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units":{"post":{"tags":["Articles"],"operationId":"addSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSalesUnitRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/deactivate":{"post":{"tags":["Articles"],"operationId":"deactivate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/activate":{"post":{"tags":["Articles"],"operationId":"activate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/roles":{"get":{"tags":["Role Management"],"summary":"List all roles (ADMIN only)","description":"Get a list of all available roles in the system. Requires USER_MANAGEMENT permission.","operationId":"listRoles","responses":{"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"200":{"description":"Roles retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RoleDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles/{roleName}":{"delete":{"tags":["User Management"],"summary":"Remove role (ADMIN only)","description":"Remove a role from a user. Requires USER_MANAGEMENT permission.","operationId":"removeRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"},"example":"user-uuid"},{"name":"roleName","in":"path","description":"Role name","required":true,"schema":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]},"example":"MANAGER"}],"responses":{"404":{"description":"User or role not found"},"403":{"description":"Missing USER_MANAGEMENT permission"},"204":{"description":"Role removed successfully"},"401":{"description":"Authentication required"}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses/{label}":{"delete":{"tags":["Customers"],"operationId":"removeDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"label","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers/{supplierId}":{"delete":{"tags":["Articles"],"operationId":"removeSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"supplierId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}":{"delete":{"tags":["Articles"],"operationId":"removeSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}}},"components":{"schemas":{"UpdateUserRequest":{"type":"object","properties":{"email":{"type":"string","description":"New email address","example":"newemail@example.com"},"branchId":{"type":"string","description":"New branch ID","example":"BRANCH-002"}},"description":"Request to update user details"},"RoleDTO":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]},"permissions":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["RECIPE_READ","RECIPE_WRITE","RECIPE_DELETE","BATCH_READ","BATCH_WRITE","BATCH_COMPLETE","BATCH_DELETE","PRODUCTION_ORDER_READ","PRODUCTION_ORDER_WRITE","PRODUCTION_ORDER_DELETE","HACCP_READ","HACCP_WRITE","TEMPERATURE_LOG_READ","TEMPERATURE_LOG_WRITE","CLEANING_RECORD_READ","CLEANING_RECORD_WRITE","GOODS_INSPECTION_READ","GOODS_INSPECTION_WRITE","STOCK_READ","STOCK_WRITE","STOCK_MOVEMENT_READ","STOCK_MOVEMENT_WRITE","INVENTORY_COUNT_READ","INVENTORY_COUNT_WRITE","PURCHASE_ORDER_READ","PURCHASE_ORDER_WRITE","PURCHASE_ORDER_DELETE","GOODS_RECEIPT_READ","GOODS_RECEIPT_WRITE","SUPPLIER_READ","SUPPLIER_WRITE","SUPPLIER_DELETE","ORDER_READ","ORDER_WRITE","ORDER_DELETE","INVOICE_READ","INVOICE_WRITE","INVOICE_DELETE","CUSTOMER_READ","CUSTOMER_WRITE","CUSTOMER_DELETE","LABEL_READ","LABEL_WRITE","LABEL_PRINT","MASTERDATA_READ","MASTERDATA_WRITE","BRANCH_READ","BRANCH_WRITE","BRANCH_DELETE","USER_READ","USER_WRITE","USER_DELETE","USER_LOCK","USER_UNLOCK","ROLE_READ","ROLE_WRITE","ROLE_ASSIGN","ROLE_REMOVE","REPORT_READ","REPORT_GENERATE","NOTIFICATION_READ","NOTIFICATION_SEND","AUDIT_LOG_READ","SYSTEM_SETTINGS_READ","SYSTEM_SETTINGS_WRITE"]}},"description":{"type":"string"}}},"UserDTO":{"type":"object","properties":{"id":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"roles":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}},"branchId":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","LOCKED"]},"createdAt":{"type":"string","format":"date-time"},"lastLogin":{"type":"string","format":"date-time"}}},"ChangePasswordRequest":{"required":["currentPassword","newPassword"],"type":"object","properties":{"currentPassword":{"type":"string","description":"Current password","example":"OldPass123"},"newPassword":{"maxLength":2147483647,"minLength":8,"type":"string","description":"New password (min 8 characters)","example":"NewSecurePass456"}},"description":"Request to change user password"},"UpdateSupplierRequest":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddressResponse":{"type":"object","properties":{"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"}}},"ContactInfoResponse":{"type":"object","properties":{"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"}}},"PaymentTermsResponse":{"type":"object","properties":{"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"QualityCertificateResponse":{"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"SupplierRatingResponse":{"type":"object","properties":{"qualityScore":{"type":"integer","format":"int32"},"deliveryScore":{"type":"integer","format":"int32"},"priceScore":{"type":"integer","format":"int32"}}},"SupplierResponse":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"certificates":{"type":"array","items":{"$ref":"#/components/schemas/QualityCertificateResponse"}},"rating":{"$ref":"#/components/schemas/SupplierRatingResponse"},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"UpdateCustomerRequest":{"type":"object","properties":{"name":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"ContractLineItemResponse":{"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string"}}},"CustomerResponse":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"billingAddress":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"deliveryAddresses":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryAddressResponse"}},"frameContract":{"$ref":"#/components/schemas/FrameContractResponse"},"preferences":{"type":"array","items":{"type":"string"}},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"DeliveryAddressResponse":{"type":"object","properties":{"label":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"FrameContractResponse":{"type":"object","properties":{"id":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"deliveryRhythm":{"type":"string"},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/ContractLineItemResponse"}}}},"SetPreferencesRequest":{"required":["preferences"],"type":"object","properties":{"preferences":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["BIO","REGIONAL","TIERWOHL","HALAL","KOSHER","GLUTENFREI","LAKTOSEFREI"]}}}},"LineItem":{"required":["agreedPrice","articleId"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]}}},"SetFrameContractRequest":{"required":["lineItems","rhythm"],"type":"object","properties":{"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"rhythm":{"type":"string","enum":["DAILY","WEEKLY","BIWEEKLY","MONTHLY","ON_DEMAND"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/LineItem"}}}},"UpdateProductCategoryRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ProductCategoryResponse":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}},"UpdateArticleRequest":{"type":"object","properties":{"name":{"type":"string"},"categoryId":{"type":"string"}}},"ArticleResponse":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"salesUnits":{"type":"array","items":{"$ref":"#/components/schemas/SalesUnitResponse"}},"status":{"type":"string"},"supplierIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SalesUnitResponse":{"type":"object","properties":{"id":{"type":"string"},"unit":{"type":"string"},"priceModel":{"type":"string"},"price":{"type":"number"}}},"UpdateSalesUnitPriceRequest":{"required":["price"],"type":"object","properties":{"price":{"type":"number"}}},"CreateUserRequest":{"required":["email","password","roleNames","username"],"type":"object","properties":{"username":{"maxLength":50,"minLength":3,"type":"string","description":"Username (unique)","example":"john.doe"},"email":{"type":"string","description":"Email address (unique)","example":"john.doe@example.com"},"password":{"maxLength":2147483647,"minLength":8,"type":"string","description":"Password (min 8 characters)","example":"SecurePass123"},"roleNames":{"uniqueItems":true,"type":"array","description":"Role names to assign","example":["USER","MANAGER"],"items":{"type":"string","description":"Role names to assign","example":"[\"USER\",\"MANAGER\"]","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"branchId":{"type":"string","description":"Branch ID (optional)","example":"BRANCH-001"}},"description":"Request to create a new user"},"AssignRoleRequest":{"required":["roleName"],"type":"object","properties":{"roleName":{"type":"string","description":"Role name to assign","example":"MANAGER","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"description":"Request to assign a role to a user"},"CreateSupplierRequest":{"required":["name","phone"],"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"RateSupplierRequest":{"type":"object","properties":{"qualityScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"deliveryScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"priceScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"}}},"AddCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"CreateCustomerRequest":{"required":["city","country","name","phone","postalCode","street","type"],"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["B2C","B2B"]},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddDeliveryAddressRequest":{"required":["city","country","postalCode","street"],"type":"object","properties":{"label":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"CreateProductCategoryRequest":{"required":["name"],"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"RefreshTokenRequest":{"required":["refreshToken"],"type":"object","properties":{"refreshToken":{"type":"string","description":"Refresh token"}},"description":"Refresh token request"},"LoginResponse":{"type":"object","properties":{"accessToken":{"type":"string","description":"JWT access token","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"tokenType":{"type":"string","description":"Token type","example":"Bearer"},"expiresIn":{"type":"integer","description":"Token expiration time in seconds","format":"int64","example":3600},"expiresAt":{"type":"string","description":"Token expiration timestamp","format":"date-time"},"refreshToken":{"type":"string","description":"Refresh token for obtaining new access token"}},"description":"Login response with JWT tokens"},"LoginRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string","description":"Username","example":"admin"},"password":{"type":"string","description":"Password","example":"admin123"}},"description":"Login request with username and password"},"CreateArticleRequest":{"required":["articleNumber","categoryId","name","price","priceModel","unit"],"type":"object","properties":{"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"AssignSupplierRequest":{"required":["supplierId"],"type":"object","properties":{"supplierId":{"type":"string"}}},"AddSalesUnitRequest":{"required":["price","priceModel","unit"],"type":"object","properties":{"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"RemoveCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"}}}},"securitySchemes":{"Bearer Authentication":{"type":"http","description":"JWT authentication token obtained from POST /api/auth/login.\n\nFormat: Bearer \n\nExample:\nBearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\n","scheme":"bearer","bearerFormat":"JWT"}}}} \ No newline at end of file diff --git a/frontend/packages/api-client/src/resources/articles.ts b/frontend/packages/api-client/src/resources/articles.ts index d39cb74..18caff7 100644 --- a/frontend/packages/api-client/src/resources/articles.ts +++ b/frontend/packages/api-client/src/resources/articles.ts @@ -6,11 +6,6 @@ * PUT /api/articles/{id}/sales-units/{suId}/price, * POST /api/articles/{id}/suppliers, DELETE /api/articles/{id}/suppliers/{supplierId} * - * NOTE: Backend returns domain objects with nested VOs: - * { "id": {"value": "uuid"}, "name": {"value": "..."}, ..., - * "supplierReferences": [{"value": "uuid"}], - * "salesUnits": [{"id": {"value":"uuid"}, "unit":"KG", "priceModel":"WEIGHT_BASED", - * "price": {"amount": 2.49, "currency": "EUR"}}] } * DELETE endpoints for sales-units and suppliers return 204 No Content → re-fetch. */ @@ -75,94 +70,50 @@ export interface UpdateSalesUnitPriceRequest { price: number; } -// ── Backend response shapes (domain objects with nested VOs) ───────────────── - -interface BackendSalesUnit { - id: { value: string }; - unit: Unit; - priceModel: PriceModel; - price: { amount: number; currency: string }; -} - -interface BackendArticle { - id: { value: string }; - name: { value: string }; - articleNumber: { value: string }; - categoryId: { value: string }; - salesUnits: BackendSalesUnit[]; - status: ArticleStatus; - supplierReferences: Array<{ value: string }>; - createdAt: string; - updatedAt: string; -} - -function mapSalesUnit(bsu: BackendSalesUnit): SalesUnitDTO { - return { - id: bsu.id.value, - unit: bsu.unit, - priceModel: bsu.priceModel, - price: bsu.price.amount, - }; -} - -function mapArticle(ba: BackendArticle): ArticleDTO { - return { - id: ba.id.value, - name: ba.name.value, - articleNumber: ba.articleNumber.value, - categoryId: ba.categoryId.value, - salesUnits: ba.salesUnits.map(mapSalesUnit), - status: ba.status, - supplierIds: ba.supplierReferences.map((sr) => sr.value), - createdAt: ba.createdAt, - updatedAt: ba.updatedAt, - }; -} - // ── Resource factory ───────────────────────────────────────────────────────── export function createArticlesResource(client: AxiosInstance) { return { async list(): Promise { - const res = await client.get('/api/articles'); - return res.data.map(mapArticle); + const res = await client.get('/api/articles'); + return res.data; }, async getById(id: string): Promise { - const res = await client.get(`/api/articles/${id}`); - return mapArticle(res.data); + const res = await client.get(`/api/articles/${id}`); + return res.data; }, async create(request: CreateArticleRequest): Promise { - const res = await client.post('/api/articles', request); - return mapArticle(res.data); + const res = await client.post('/api/articles', request); + return res.data; }, async update(id: string, request: UpdateArticleRequest): Promise { - const res = await client.put(`/api/articles/${id}`, request); - return mapArticle(res.data); + const res = await client.put(`/api/articles/${id}`, request); + return res.data; }, async activate(id: string): Promise { - const res = await client.post(`/api/articles/${id}/activate`); - return mapArticle(res.data); + const res = await client.post(`/api/articles/${id}/activate`); + return res.data; }, async deactivate(id: string): Promise { - const res = await client.post(`/api/articles/${id}/deactivate`); - return mapArticle(res.data); + const res = await client.post(`/api/articles/${id}/deactivate`); + return res.data; }, async addSalesUnit(id: string, request: AddSalesUnitRequest): Promise { - const res = await client.post(`/api/articles/${id}/sales-units`, request); - return mapArticle(res.data); + const res = await client.post(`/api/articles/${id}/sales-units`, request); + return res.data; }, // Returns 204 No Content → re-fetch article async removeSalesUnit(articleId: string, salesUnitId: string): Promise { await client.delete(`/api/articles/${articleId}/sales-units/${salesUnitId}`); - const res = await client.get(`/api/articles/${articleId}`); - return mapArticle(res.data); + const res = await client.get(`/api/articles/${articleId}`); + return res.data; }, async updateSalesUnitPrice( @@ -170,25 +121,25 @@ export function createArticlesResource(client: AxiosInstance) { salesUnitId: string, request: UpdateSalesUnitPriceRequest, ): Promise { - const res = await client.put( + const res = await client.put( `/api/articles/${articleId}/sales-units/${salesUnitId}/price`, request, ); - return mapArticle(res.data); + return res.data; }, async assignSupplier(articleId: string, supplierId: string): Promise { - const res = await client.post(`/api/articles/${articleId}/suppliers`, { + const res = await client.post(`/api/articles/${articleId}/suppliers`, { supplierId, }); - return mapArticle(res.data); + return res.data; }, // Returns 204 No Content → re-fetch article async removeSupplier(articleId: string, supplierId: string): Promise { await client.delete(`/api/articles/${articleId}/suppliers/${supplierId}`); - const res = await client.get(`/api/articles/${articleId}`); - return mapArticle(res.data); + const res = await client.get(`/api/articles/${articleId}`); + return res.data; }, }; } diff --git a/frontend/packages/api-client/src/resources/categories.ts b/frontend/packages/api-client/src/resources/categories.ts index 9749a96..0bcc9e0 100644 --- a/frontend/packages/api-client/src/resources/categories.ts +++ b/frontend/packages/api-client/src/resources/categories.ts @@ -2,9 +2,8 @@ * Categories resource – Real HTTP implementation. * Endpoints: GET/POST /api/categories, PUT/DELETE /api/categories/{id} * - * NOTE: The backend returns domain objects serialized with Jackson field-visibility. - * VOs like ProductCategoryId and CategoryName serialize as nested records: - * { "id": {"value": "uuid"}, "name": {"value": "string"}, "description": "string|null" } + * Backend returns ProductCategoryResponse records: + * { "id": "uuid", "name": "string", "description": "string|null" } */ import type { AxiosInstance } from 'axios'; @@ -25,47 +24,30 @@ export interface UpdateCategoryRequest { description?: string | null; } -// ── Backend response shapes (domain objects with nested VOs) ───────────────── - -interface BackendProductCategory { - id: { value: string }; - name: { value: string }; - description: string | null; -} - -function mapCategory(bc: BackendProductCategory): ProductCategoryDTO { - return { - id: bc.id.value, - name: bc.name.value, - description: bc.description, - }; -} - // ── Resource factory ───────────────────────────────────────────────────────── export function createCategoriesResource(client: AxiosInstance) { return { async list(): Promise { - const res = await client.get('/api/categories'); - return res.data.map(mapCategory); + const res = await client.get('/api/categories'); + return res.data; }, - // No GET /api/categories/{id} endpoint – implemented as list + filter async getById(id: string): Promise { - const res = await client.get('/api/categories'); - const cat = res.data.find((c) => c.id.value === id); + const res = await client.get('/api/categories'); + const cat = res.data.find((c) => c.id === id); if (!cat) throw new Error(`Kategorie nicht gefunden: ${id}`); - return mapCategory(cat); + return cat; }, async create(request: CreateCategoryRequest): Promise { - const res = await client.post('/api/categories', request); - return mapCategory(res.data); + const res = await client.post('/api/categories', request); + return res.data; }, async update(id: string, request: UpdateCategoryRequest): Promise { - const res = await client.put(`/api/categories/${id}`, request); - return mapCategory(res.data); + const res = await client.put(`/api/categories/${id}`, request); + return res.data; }, async delete(id: string): Promise { diff --git a/frontend/packages/api-client/src/resources/customers.ts b/frontend/packages/api-client/src/resources/customers.ts index 66801e0..0a72fb4 100644 --- a/frontend/packages/api-client/src/resources/customers.ts +++ b/frontend/packages/api-client/src/resources/customers.ts @@ -8,14 +8,6 @@ * DELETE /api/customers/{id}/frame-contract, * PUT /api/customers/{id}/preferences * - * NOTE: Backend returns domain objects with nested VOs: - * { "id": {"value":"uuid"}, "name": {"value":"string"}, - * "billingAddress": {street, houseNumber, postalCode, city, country}, - * "contactInfo": {phone, email, contactPerson}, - * "paymentTerms": {paymentDueDays, description}, - * "deliveryAddresses": [{label, address: {...}, contactPerson, deliveryNotes}], - * "frameContract": {"id": {"value":"uuid"}, validFrom, validUntil, deliveryRhythm, lineItems}, - * "preferences": ["BIO", ...], "status": "ACTIVE", ... } * DELETE delivery-addresses/{label} and DELETE frame-contract return 204 → re-fetch. */ @@ -143,153 +135,75 @@ export interface SetFrameContractRequest { lineItems: SetFrameContractLineItem[]; } -// ── Backend response shapes (domain objects with nested VOs) ───────────────── - -interface BackendPaymentTerms { - paymentDueDays: number; - description: string | null; // Note: backend field is "description", not "paymentDescription" -} - -interface BackendContractLineItem { - articleId: { value: string }; - agreedPrice: { amount: number; currency: string }; - agreedQuantity: number | null; - unit: string | null; -} - -interface BackendFrameContract { - id: { value: string }; - validFrom: string | null; - validUntil: string | null; - deliveryRhythm: DeliveryRhythm; - lineItems: BackendContractLineItem[]; -} - -interface BackendCustomer { - id: { value: string }; - name: { value: string }; - type: CustomerType; - status: CustomerStatus; - billingAddress: AddressDTO; - contactInfo: ContactInfoDTO; - paymentTerms: BackendPaymentTerms | null; - deliveryAddresses: DeliveryAddressDTO[]; // DeliveryAddress is a record → matches DTO shape - frameContract: BackendFrameContract | null; - preferences: CustomerPreference[]; - createdAt: string; - updatedAt: string; -} - -function mapLineItem(bli: BackendContractLineItem): ContractLineItemDTO { - return { - articleId: bli.articleId.value, - agreedPrice: bli.agreedPrice.amount, - agreedQuantity: bli.agreedQuantity, - unit: bli.unit, - }; -} - -function mapFrameContract(bfc: BackendFrameContract): FrameContractDTO { - return { - id: bfc.id.value, - validFrom: bfc.validFrom, - validUntil: bfc.validUntil, - deliveryRhythm: bfc.deliveryRhythm, - lineItems: bfc.lineItems.map(mapLineItem), - }; -} - -function mapCustomer(bc: BackendCustomer): CustomerDTO { - return { - id: bc.id.value, - name: bc.name.value, - type: bc.type, - status: bc.status, - billingAddress: bc.billingAddress, - contactInfo: bc.contactInfo, - paymentTerms: bc.paymentTerms - ? { - paymentDueDays: bc.paymentTerms.paymentDueDays, - paymentDescription: bc.paymentTerms.description, - } - : null, - deliveryAddresses: bc.deliveryAddresses, - frameContract: bc.frameContract ? mapFrameContract(bc.frameContract) : null, - preferences: bc.preferences, - createdAt: bc.createdAt, - updatedAt: bc.updatedAt, - }; -} - // ── Resource factory ───────────────────────────────────────────────────────── export function createCustomersResource(client: AxiosInstance) { return { async list(): Promise { - const res = await client.get('/api/customers'); - return res.data.map(mapCustomer); + const res = await client.get('/api/customers'); + return res.data; }, async getById(id: string): Promise { - const res = await client.get(`/api/customers/${id}`); - return mapCustomer(res.data); + const res = await client.get(`/api/customers/${id}`); + return res.data; }, async create(request: CreateCustomerRequest): Promise { - const res = await client.post('/api/customers', request); - return mapCustomer(res.data); + const res = await client.post('/api/customers', request); + return res.data; }, async update(id: string, request: UpdateCustomerRequest): Promise { - const res = await client.put(`/api/customers/${id}`, request); - return mapCustomer(res.data); + const res = await client.put(`/api/customers/${id}`, request); + return res.data; }, async activate(id: string): Promise { - const res = await client.post(`/api/customers/${id}/activate`); - return mapCustomer(res.data); + const res = await client.post(`/api/customers/${id}/activate`); + return res.data; }, async deactivate(id: string): Promise { - const res = await client.post(`/api/customers/${id}/deactivate`); - return mapCustomer(res.data); + const res = await client.post(`/api/customers/${id}/deactivate`); + return res.data; }, async addDeliveryAddress(id: string, request: AddDeliveryAddressRequest): Promise { - const res = await client.post( + const res = await client.post( `/api/customers/${id}/delivery-addresses`, request, ); - return mapCustomer(res.data); + return res.data; }, // Returns 204 No Content → re-fetch customer async removeDeliveryAddress(id: string, label: string): Promise { await client.delete(`/api/customers/${id}/delivery-addresses/${encodeURIComponent(label)}`); - const res = await client.get(`/api/customers/${id}`); - return mapCustomer(res.data); + const res = await client.get(`/api/customers/${id}`); + return res.data; }, async setFrameContract(id: string, request: SetFrameContractRequest): Promise { - const res = await client.put( + const res = await client.put( `/api/customers/${id}/frame-contract`, request, ); - return mapCustomer(res.data); + return res.data; }, // Returns 204 No Content → re-fetch customer async removeFrameContract(id: string): Promise { await client.delete(`/api/customers/${id}/frame-contract`); - const res = await client.get(`/api/customers/${id}`); - return mapCustomer(res.data); + const res = await client.get(`/api/customers/${id}`); + return res.data; }, async setPreferences(id: string, preferences: CustomerPreference[]): Promise { - const res = await client.put(`/api/customers/${id}/preferences`, { + const res = await client.put(`/api/customers/${id}/preferences`, { preferences, }); - return mapCustomer(res.data); + return res.data; }, }; } diff --git a/frontend/packages/api-client/src/resources/suppliers.ts b/frontend/packages/api-client/src/resources/suppliers.ts index 5314d86..f953ac2 100644 --- a/frontend/packages/api-client/src/resources/suppliers.ts +++ b/frontend/packages/api-client/src/resources/suppliers.ts @@ -6,14 +6,6 @@ * POST /api/suppliers/{id}/certificates, * DELETE /api/suppliers/{id}/certificates (with body) * - * NOTE: Backend returns domain objects with nested VOs: - * { "id": {"value":"uuid"}, "name": {"value":"string"}, - * "address": {"street":"...","houseNumber":"...","postalCode":"...","city":"...","country":"DE"}, - * "contactInfo": {"phone":"...","email":"...","contactPerson":"..."}, - * "paymentTerms": {"paymentDueDays":30,"description":"..."}, - * "certificates": [{"certificateType":"...","issuer":"...","validFrom":"2024-01-01","validUntil":"2026-12-31"}], - * "rating": {"qualityScore":4,"deliveryScore":4,"priceScore":5}, - * "status": "ACTIVE", "createdAt":"...", "updatedAt":"..." } * DELETE /api/suppliers/{id}/certificates returns 204 No Content → re-fetch. */ @@ -113,122 +105,55 @@ export interface RemoveCertificateRequest { validFrom: string; } -// ── Backend response shapes (domain objects with nested VOs) ───────────────── - -interface BackendAddress { - street: string; - houseNumber: string | null; - postalCode: string; - city: string; - country: string; -} - -interface BackendContactInfo { - phone: string; - email: string | null; - contactPerson: string | null; -} - -interface BackendPaymentTerms { - paymentDueDays: number; - description: string | null; // Note: backend field is "description", not "paymentDescription" -} - -interface BackendQualityCertificate { - certificateType: string; - issuer: string; - validFrom: string; // LocalDate → "2024-01-01" - validUntil: string; -} - -interface BackendSupplierRating { - qualityScore: number; - deliveryScore: number; - priceScore: number; -} - -interface BackendSupplier { - id: { value: string }; - name: { value: string }; - address: BackendAddress | null; - contactInfo: BackendContactInfo; - paymentTerms: BackendPaymentTerms | null; - certificates: BackendQualityCertificate[]; - rating: BackendSupplierRating | null; - status: SupplierStatus; - createdAt: string; - updatedAt: string; -} - -function mapSupplier(bs: BackendSupplier): SupplierDTO { - return { - id: bs.id.value, - name: bs.name.value, - status: bs.status, - address: bs.address, - contactInfo: bs.contactInfo, - paymentTerms: bs.paymentTerms - ? { - paymentDueDays: bs.paymentTerms.paymentDueDays, - paymentDescription: bs.paymentTerms.description, - } - : null, - certificates: bs.certificates, - rating: bs.rating, - createdAt: bs.createdAt, - updatedAt: bs.updatedAt, - }; -} - // ── Resource factory ───────────────────────────────────────────────────────── export function createSuppliersResource(client: AxiosInstance) { return { async list(): Promise { - const res = await client.get('/api/suppliers'); - return res.data.map(mapSupplier); + const res = await client.get('/api/suppliers'); + return res.data; }, async getById(id: string): Promise { - const res = await client.get(`/api/suppliers/${id}`); - return mapSupplier(res.data); + const res = await client.get(`/api/suppliers/${id}`); + return res.data; }, async create(request: CreateSupplierRequest): Promise { - const res = await client.post('/api/suppliers', request); - return mapSupplier(res.data); + const res = await client.post('/api/suppliers', request); + return res.data; }, async update(id: string, request: UpdateSupplierRequest): Promise { - const res = await client.put(`/api/suppliers/${id}`, request); - return mapSupplier(res.data); + const res = await client.put(`/api/suppliers/${id}`, request); + return res.data; }, async activate(id: string): Promise { - const res = await client.post(`/api/suppliers/${id}/activate`); - return mapSupplier(res.data); + const res = await client.post(`/api/suppliers/${id}/activate`); + return res.data; }, async deactivate(id: string): Promise { - const res = await client.post(`/api/suppliers/${id}/deactivate`); - return mapSupplier(res.data); + const res = await client.post(`/api/suppliers/${id}/deactivate`); + return res.data; }, async rate(id: string, request: RateSupplierRequest): Promise { - const res = await client.post(`/api/suppliers/${id}/rating`, request); - return mapSupplier(res.data); + const res = await client.post(`/api/suppliers/${id}/rating`, request); + return res.data; }, async addCertificate(id: string, request: AddCertificateRequest): Promise { - const res = await client.post(`/api/suppliers/${id}/certificates`, request); - return mapSupplier(res.data); + const res = await client.post(`/api/suppliers/${id}/certificates`, request); + return res.data; }, // Returns 204 No Content → re-fetch supplier async removeCertificate(id: string, request: RemoveCertificateRequest): Promise { await client.delete(`/api/suppliers/${id}/certificates`, { data: request }); - const res = await client.get(`/api/suppliers/${id}`); - return mapSupplier(res.data); + const res = await client.get(`/api/suppliers/${id}`); + return res.data; }, }; }