1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 08:29:36 +01:00

fix(masterdata): MASTERDATA-Permissions und JSON-Serialisierung der REST-Responses

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.
This commit is contained in:
Sebastian Frick 2026-02-18 22:13:23 +01:00
parent 3cccab1f4d
commit fbed3f899f
26 changed files with 481 additions and 364 deletions

View file

@ -79,6 +79,10 @@ public enum Permission {
LABEL_WRITE,
LABEL_PRINT,
// ==================== Master Data BC ====================
MASTERDATA_READ,
MASTERDATA_WRITE,
// ==================== Filiales BC ====================
BRANCH_READ,
BRANCH_WRITE,

View file

@ -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<Article> createArticle(
public ResponseEntity<ArticleResponse> 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<List<Article>> listArticles(
public ResponseEntity<List<ArticleResponse>> 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<Article> getArticle(
public ResponseEntity<ArticleResponse> 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<Article> updateArticle(
public ResponseEntity<ArticleResponse> 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<Article> activate(
public ResponseEntity<ArticleResponse> 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<Article> deactivate(
public ResponseEntity<ArticleResponse> 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<Article> addSalesUnit(
public ResponseEntity<ArticleResponse> 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<Article> updateSalesUnitPrice(
public ResponseEntity<ArticleResponse> 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<Article> assignSupplier(
public ResponseEntity<ArticleResponse> 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}")

View file

@ -72,7 +72,7 @@ public class CustomerController {
@PostMapping
@PreAuthorize("hasAuthority('MASTERDATA_WRITE')")
public ResponseEntity<Customer> createCustomer(
public ResponseEntity<CustomerResponse> 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<List<Customer>> listCustomers(
public ResponseEntity<List<CustomerResponse>> 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<Customer> getCustomer(
public ResponseEntity<CustomerResponse> 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<Customer> updateCustomer(
public ResponseEntity<CustomerResponse> 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<Customer> activate(
public ResponseEntity<CustomerResponse> 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<Customer> deactivate(
public ResponseEntity<CustomerResponse> 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<Customer> addDeliveryAddress(
public ResponseEntity<CustomerResponse> 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<Customer> setFrameContract(
public ResponseEntity<CustomerResponse> 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<Customer> setPreferences(
public ResponseEntity<CustomerResponse> 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) {

View file

@ -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<ProductCategory> createCategory(
public ResponseEntity<ProductCategoryResponse> 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<List<ProductCategory>> listCategories(Authentication authentication) {
public ResponseEntity<List<ProductCategoryResponse>> 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<ProductCategory> updateCategory(
public ResponseEntity<ProductCategoryResponse> 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}")

View file

@ -64,7 +64,7 @@ public class SupplierController {
@PostMapping
@PreAuthorize("hasAuthority('MASTERDATA_WRITE')")
public ResponseEntity<Supplier> createSupplier(
public ResponseEntity<SupplierResponse> 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<List<Supplier>> listSuppliers(
public ResponseEntity<List<SupplierResponse>> 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<Supplier> getSupplier(
public ResponseEntity<SupplierResponse> 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<Supplier> updateSupplier(
public ResponseEntity<SupplierResponse> 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<Supplier> activate(
public ResponseEntity<SupplierResponse> 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<Supplier> deactivate(
public ResponseEntity<SupplierResponse> 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<Supplier> rateSupplier(
public ResponseEntity<SupplierResponse> 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<Supplier> addCertificate(
public ResponseEntity<SupplierResponse> 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")

View file

@ -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()
);
}
}

View file

@ -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<SalesUnitResponse> salesUnits,
String status,
List<String> 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()
);
}
}

View file

@ -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());
}
}

View file

@ -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()
);
}
}

View file

@ -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<DeliveryAddressResponse> deliveryAddresses,
FrameContractResponse frameContract,
List<String> 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()
);
}
}

View file

@ -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()
);
}
}

View file

@ -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<ContractLineItemResponse> 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()
);
}
}

View file

@ -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());
}
}

View file

@ -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()
);
}
}

View file

@ -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()
);
}
}

View file

@ -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()
);
}
}

View file

@ -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()
);
}
}

View file

@ -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<QualityCertificateResponse> 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()
);
}
}

View file

@ -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');

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="008-add-masterdata-permissions" author="effigenix">
<sqlFile path="db/changelog/changes/008-add-masterdata-permissions.sql"/>
</changeSet>
</databaseChangeLog>

View file

@ -12,5 +12,6 @@
<include file="db/changelog/changes/005-create-masterdata-schema.xml"/>
<include file="db/changelog/changes/006-create-supplier-schema.xml"/>
<include file="db/changelog/changes/007-create-customer-schema.xml"/>
<include file="db/changelog/changes/008-add-masterdata-permissions.xml"/>
</databaseChangeLog>

File diff suppressed because one or more lines are too long

View file

@ -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<ArticleDTO[]> {
const res = await client.get<BackendArticle[]>('/api/articles');
return res.data.map(mapArticle);
const res = await client.get<ArticleDTO[]>('/api/articles');
return res.data;
},
async getById(id: string): Promise<ArticleDTO> {
const res = await client.get<BackendArticle>(`/api/articles/${id}`);
return mapArticle(res.data);
const res = await client.get<ArticleDTO>(`/api/articles/${id}`);
return res.data;
},
async create(request: CreateArticleRequest): Promise<ArticleDTO> {
const res = await client.post<BackendArticle>('/api/articles', request);
return mapArticle(res.data);
const res = await client.post<ArticleDTO>('/api/articles', request);
return res.data;
},
async update(id: string, request: UpdateArticleRequest): Promise<ArticleDTO> {
const res = await client.put<BackendArticle>(`/api/articles/${id}`, request);
return mapArticle(res.data);
const res = await client.put<ArticleDTO>(`/api/articles/${id}`, request);
return res.data;
},
async activate(id: string): Promise<ArticleDTO> {
const res = await client.post<BackendArticle>(`/api/articles/${id}/activate`);
return mapArticle(res.data);
const res = await client.post<ArticleDTO>(`/api/articles/${id}/activate`);
return res.data;
},
async deactivate(id: string): Promise<ArticleDTO> {
const res = await client.post<BackendArticle>(`/api/articles/${id}/deactivate`);
return mapArticle(res.data);
const res = await client.post<ArticleDTO>(`/api/articles/${id}/deactivate`);
return res.data;
},
async addSalesUnit(id: string, request: AddSalesUnitRequest): Promise<ArticleDTO> {
const res = await client.post<BackendArticle>(`/api/articles/${id}/sales-units`, request);
return mapArticle(res.data);
const res = await client.post<ArticleDTO>(`/api/articles/${id}/sales-units`, request);
return res.data;
},
// Returns 204 No Content → re-fetch article
async removeSalesUnit(articleId: string, salesUnitId: string): Promise<ArticleDTO> {
await client.delete(`/api/articles/${articleId}/sales-units/${salesUnitId}`);
const res = await client.get<BackendArticle>(`/api/articles/${articleId}`);
return mapArticle(res.data);
const res = await client.get<ArticleDTO>(`/api/articles/${articleId}`);
return res.data;
},
async updateSalesUnitPrice(
@ -170,25 +121,25 @@ export function createArticlesResource(client: AxiosInstance) {
salesUnitId: string,
request: UpdateSalesUnitPriceRequest,
): Promise<ArticleDTO> {
const res = await client.put<BackendArticle>(
const res = await client.put<ArticleDTO>(
`/api/articles/${articleId}/sales-units/${salesUnitId}/price`,
request,
);
return mapArticle(res.data);
return res.data;
},
async assignSupplier(articleId: string, supplierId: string): Promise<ArticleDTO> {
const res = await client.post<BackendArticle>(`/api/articles/${articleId}/suppliers`, {
const res = await client.post<ArticleDTO>(`/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<ArticleDTO> {
await client.delete(`/api/articles/${articleId}/suppliers/${supplierId}`);
const res = await client.get<BackendArticle>(`/api/articles/${articleId}`);
return mapArticle(res.data);
const res = await client.get<ArticleDTO>(`/api/articles/${articleId}`);
return res.data;
},
};
}

View file

@ -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<ProductCategoryDTO[]> {
const res = await client.get<BackendProductCategory[]>('/api/categories');
return res.data.map(mapCategory);
const res = await client.get<ProductCategoryDTO[]>('/api/categories');
return res.data;
},
// No GET /api/categories/{id} endpoint implemented as list + filter
async getById(id: string): Promise<ProductCategoryDTO> {
const res = await client.get<BackendProductCategory[]>('/api/categories');
const cat = res.data.find((c) => c.id.value === id);
const res = await client.get<ProductCategoryDTO[]>('/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<ProductCategoryDTO> {
const res = await client.post<BackendProductCategory>('/api/categories', request);
return mapCategory(res.data);
const res = await client.post<ProductCategoryDTO>('/api/categories', request);
return res.data;
},
async update(id: string, request: UpdateCategoryRequest): Promise<ProductCategoryDTO> {
const res = await client.put<BackendProductCategory>(`/api/categories/${id}`, request);
return mapCategory(res.data);
const res = await client.put<ProductCategoryDTO>(`/api/categories/${id}`, request);
return res.data;
},
async delete(id: string): Promise<void> {

View file

@ -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<CustomerDTO[]> {
const res = await client.get<BackendCustomer[]>('/api/customers');
return res.data.map(mapCustomer);
const res = await client.get<CustomerDTO[]>('/api/customers');
return res.data;
},
async getById(id: string): Promise<CustomerDTO> {
const res = await client.get<BackendCustomer>(`/api/customers/${id}`);
return mapCustomer(res.data);
const res = await client.get<CustomerDTO>(`/api/customers/${id}`);
return res.data;
},
async create(request: CreateCustomerRequest): Promise<CustomerDTO> {
const res = await client.post<BackendCustomer>('/api/customers', request);
return mapCustomer(res.data);
const res = await client.post<CustomerDTO>('/api/customers', request);
return res.data;
},
async update(id: string, request: UpdateCustomerRequest): Promise<CustomerDTO> {
const res = await client.put<BackendCustomer>(`/api/customers/${id}`, request);
return mapCustomer(res.data);
const res = await client.put<CustomerDTO>(`/api/customers/${id}`, request);
return res.data;
},
async activate(id: string): Promise<CustomerDTO> {
const res = await client.post<BackendCustomer>(`/api/customers/${id}/activate`);
return mapCustomer(res.data);
const res = await client.post<CustomerDTO>(`/api/customers/${id}/activate`);
return res.data;
},
async deactivate(id: string): Promise<CustomerDTO> {
const res = await client.post<BackendCustomer>(`/api/customers/${id}/deactivate`);
return mapCustomer(res.data);
const res = await client.post<CustomerDTO>(`/api/customers/${id}/deactivate`);
return res.data;
},
async addDeliveryAddress(id: string, request: AddDeliveryAddressRequest): Promise<CustomerDTO> {
const res = await client.post<BackendCustomer>(
const res = await client.post<CustomerDTO>(
`/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<CustomerDTO> {
await client.delete(`/api/customers/${id}/delivery-addresses/${encodeURIComponent(label)}`);
const res = await client.get<BackendCustomer>(`/api/customers/${id}`);
return mapCustomer(res.data);
const res = await client.get<CustomerDTO>(`/api/customers/${id}`);
return res.data;
},
async setFrameContract(id: string, request: SetFrameContractRequest): Promise<CustomerDTO> {
const res = await client.put<BackendCustomer>(
const res = await client.put<CustomerDTO>(
`/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<CustomerDTO> {
await client.delete(`/api/customers/${id}/frame-contract`);
const res = await client.get<BackendCustomer>(`/api/customers/${id}`);
return mapCustomer(res.data);
const res = await client.get<CustomerDTO>(`/api/customers/${id}`);
return res.data;
},
async setPreferences(id: string, preferences: CustomerPreference[]): Promise<CustomerDTO> {
const res = await client.put<BackendCustomer>(`/api/customers/${id}/preferences`, {
const res = await client.put<CustomerDTO>(`/api/customers/${id}/preferences`, {
preferences,
});
return mapCustomer(res.data);
return res.data;
},
};
}

View file

@ -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<SupplierDTO[]> {
const res = await client.get<BackendSupplier[]>('/api/suppliers');
return res.data.map(mapSupplier);
const res = await client.get<SupplierDTO[]>('/api/suppliers');
return res.data;
},
async getById(id: string): Promise<SupplierDTO> {
const res = await client.get<BackendSupplier>(`/api/suppliers/${id}`);
return mapSupplier(res.data);
const res = await client.get<SupplierDTO>(`/api/suppliers/${id}`);
return res.data;
},
async create(request: CreateSupplierRequest): Promise<SupplierDTO> {
const res = await client.post<BackendSupplier>('/api/suppliers', request);
return mapSupplier(res.data);
const res = await client.post<SupplierDTO>('/api/suppliers', request);
return res.data;
},
async update(id: string, request: UpdateSupplierRequest): Promise<SupplierDTO> {
const res = await client.put<BackendSupplier>(`/api/suppliers/${id}`, request);
return mapSupplier(res.data);
const res = await client.put<SupplierDTO>(`/api/suppliers/${id}`, request);
return res.data;
},
async activate(id: string): Promise<SupplierDTO> {
const res = await client.post<BackendSupplier>(`/api/suppliers/${id}/activate`);
return mapSupplier(res.data);
const res = await client.post<SupplierDTO>(`/api/suppliers/${id}/activate`);
return res.data;
},
async deactivate(id: string): Promise<SupplierDTO> {
const res = await client.post<BackendSupplier>(`/api/suppliers/${id}/deactivate`);
return mapSupplier(res.data);
const res = await client.post<SupplierDTO>(`/api/suppliers/${id}/deactivate`);
return res.data;
},
async rate(id: string, request: RateSupplierRequest): Promise<SupplierDTO> {
const res = await client.post<BackendSupplier>(`/api/suppliers/${id}/rating`, request);
return mapSupplier(res.data);
const res = await client.post<SupplierDTO>(`/api/suppliers/${id}/rating`, request);
return res.data;
},
async addCertificate(id: string, request: AddCertificateRequest): Promise<SupplierDTO> {
const res = await client.post<BackendSupplier>(`/api/suppliers/${id}/certificates`, request);
return mapSupplier(res.data);
const res = await client.post<SupplierDTO>(`/api/suppliers/${id}/certificates`, request);
return res.data;
},
// Returns 204 No Content → re-fetch supplier
async removeCertificate(id: string, request: RemoveCertificateRequest): Promise<SupplierDTO> {
await client.delete(`/api/suppliers/${id}/certificates`, { data: request });
const res = await client.get<BackendSupplier>(`/api/suppliers/${id}`);
return mapSupplier(res.data);
const res = await client.get<SupplierDTO>(`/api/suppliers/${id}`);
return res.data;
},
};
}