This commit is contained in:
2026-08-20 18:02:38 +08:00
parent b55b049e77
commit 1cd8b6a473
210 changed files with 6974 additions and 825 deletions
@@ -6,10 +6,13 @@ import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.procurement.dto.OverseasProcurementRequests;
import com.baiyue.lastleg.procurement.dto.OverseasProcurementResponses;
import com.baiyue.lastleg.procurement.service.OverseasProcurementService;
import com.baiyue.lastleg.security.OverseasPermissions;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -20,12 +23,16 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@RestController
@RequestMapping("/api/platform/v1/last-leg/overseas-procurements")
public class PlatformOverseasProcurementController {
@Resource private OverseasProcurementService service;
@GetMapping
@PreAuthorize("hasAuthority('" + OverseasPermissions.PURCHASE_LIST + "')")
@Operation(summary = "Page overseas procurement orders", description = "Required authority: " + OverseasPermissions.PURCHASE_LIST)
public ApiResponse<PageResult<OverseasProcurementResponses.Summary>> page(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@@ -34,7 +41,29 @@ public class PlatformOverseasProcurementController {
return ApiResponse.success(service.page(trim(customerId), query));
}
@GetMapping("/customer-options")
@PreAuthorize("hasAuthority('" + OverseasPermissions.PURCHASE_LIST + "')")
@Operation(summary = "List procurement customer options", description = "Required authority: " + OverseasPermissions.PURCHASE_LIST)
public ApiResponse<List<OverseasProcurementResponses.CustomerOption>> customerOptions(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "50") int limit) {
requirePlatform(user);
return ApiResponse.success(service.customerOptions(keyword, limit));
}
@GetMapping("/warehouse-options")
@PreAuthorize("hasAuthority('" + OverseasPermissions.PURCHASE_LIST + "')")
@Operation(summary = "List procurement warehouse options", description = "Required authority: " + OverseasPermissions.PURCHASE_LIST)
public ApiResponse<List<OverseasProcurementResponses.WarehouseOption>> warehouseOptions(
@AuthenticationPrincipal AuthenticatedUser user) {
requirePlatform(user);
return ApiResponse.success(service.warehouseOptions());
}
@GetMapping("/{orderNo}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.PURCHASE_LIST + "')")
@Operation(summary = "Get an overseas procurement order", description = "Required authority: " + OverseasPermissions.PURCHASE_LIST)
public ApiResponse<OverseasProcurementResponses.Detail> detail(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId,
@PathVariable String orderNo) {
@@ -43,6 +72,8 @@ public class PlatformOverseasProcurementController {
}
@PostMapping("/{orderNo}/review")
@PreAuthorize("hasAuthority('" + OverseasPermissions.PURCHASE_REVIEW + "')")
@Operation(summary = "Review an overseas procurement order", description = "Required authority: " + OverseasPermissions.PURCHASE_REVIEW)
public ApiResponse<OverseasProcurementResponses.Detail> review(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId,
@PathVariable String orderNo, @Valid @RequestBody OverseasProcurementRequests.Review request) {
@@ -51,19 +82,13 @@ public class PlatformOverseasProcurementController {
}
@PatchMapping("/{orderNo}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.PURCHASE_EDIT + "')")
@Operation(summary = "Update an overseas procurement order", description = "Required authority: " + OverseasPermissions.PURCHASE_EDIT)
public ApiResponse<OverseasProcurementResponses.Detail> update(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId,
@PathVariable String orderNo, @Valid @RequestBody OverseasProcurementRequests.Update request) {
requirePermission(user, OverseasProcurementService.MANAGE_PERMISSION);
return ApiResponse.success(service.update(requiredCustomer(customerId), orderNo, request));
}
@PostMapping("/{orderNo}/outbounds")
public ApiResponse<OverseasProcurementResponses.Detail> addOutbound(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId,
@PathVariable String orderNo, @Valid @RequestBody OverseasProcurementRequests.AddOutbound request) {
requirePermission(user, OverseasProcurementService.MANAGE_PERMISSION);
return ApiResponse.success(service.addOutbound(requiredCustomer(customerId), orderNo, operator(user), request));
return ApiResponse.success(service.update(requiredCustomer(customerId), orderNo, operator(user), request));
}
private void requirePlatform(AuthenticatedUser user) {
@@ -1,5 +1,6 @@
package com.baiyue.lastleg.procurement.dto;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
@@ -41,23 +42,15 @@ public final class OverseasProcurementRequests {
@NotNull @Min(0) Long version,
@Size(max = 128) String purchaseCategory,
@Size(max = 255) String productName,
@Size(max = 500) String productSpecification,
@Size(max = 2000) String productUrl,
JsonNode productSpecification,
JsonNode productUrl,
@Min(0) Long inboundQuantity,
@Size(max = 128) String purchaseWarehouseId,
@Size(max = 255) String purchaseWarehouseName,
@Size(max = 128) String domesticCarrier,
@Size(max = 128) String domesticTrackingNo,
@Size(max = 128) String overseasCarrier,
@Size(max = 128) String overseasTrackingNo,
LocalDateTime estimatedArrivalAt,
@Size(max = 1000) String remark) {}
public record AddOutbound(
@NotNull @Positive Long quantity,
@NotBlank @Size(max = 128) String outboundNo,
@Size(max = 128) String carrier,
@Size(max = 128) String trackingNo,
LocalDateTime outboundAt,
@Size(max = 1000) String remark) {}
JsonNode purchaseWarehouseId,
JsonNode purchaseWarehouseName,
JsonNode domesticCarrier,
JsonNode domesticTrackingNo,
JsonNode overseasCarrier,
JsonNode overseasTrackingNo,
JsonNode estimatedArrivalAt,
JsonNode remark) {}
}
@@ -12,15 +12,17 @@ public final class OverseasProcurementResponses {
String purchaseCategory, String productName, String productSpecification,
long purchaseQuantity, BigDecimal budgetUnitPrice, String currency,
String purchaseWarehouseId, String purchaseWarehouseName,
long inboundQuantity, long outboundQuantity, long inventoryQuantity,
long inboundQuantity, long inventoryQuantity,
String status, long version, LocalDateTime createdAt, LocalDateTime updatedAt) {}
public record Outbound(
Long id, long quantity, String outboundNo, String carrier, String trackingNo,
LocalDateTime outboundAt, String remark, String createdBy, LocalDateTime createdAt) {}
public record CustomerOption(String customerId, String customerName) {}
public record WarehouseOption(
String warehouseId, String warehouseCode, String warehouseName, String countryCode) {}
public record History(
String fromStatus, String toStatus, String operatorId, String remark,
String operationType, String fromStatus, String toStatus, String operatorId,
String changeDetail, String remark,
LocalDateTime createdAt) {}
public record Detail(
@@ -30,5 +32,5 @@ public final class OverseasProcurementResponses {
String overseasCarrier, String overseasTrackingNo,
LocalDateTime estimatedArrivalAt, String remark,
String createdBy, String reviewedBy, String reviewRemark, LocalDateTime reviewedAt,
List<Outbound> outbounds, List<History> histories) {}
List<History> histories) {}
}
@@ -28,7 +28,6 @@ public class OverseasProcurementOrder {
private String purchaseWarehouseId;
private String purchaseWarehouseName;
private Long inboundQuantity;
private Long outboundQuantity;
private String domesticCarrier;
private String domesticTrackingNo;
private String overseasCarrier;
@@ -1,24 +0,0 @@
package com.baiyue.lastleg.procurement.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_overseas_procurement_outbound")
public class OverseasProcurementOutbound {
@TableId(type = IdType.INPUT) private Long id;
private Long orderId;
private String customerId;
private Long quantity;
private String outboundNo;
private String carrier;
private String trackingNo;
private LocalDateTime outboundAt;
private String remark;
private String createdBy;
private LocalDateTime createdAt;
}
@@ -13,9 +13,11 @@ public class OverseasProcurementStatusHistory {
@TableId(type = IdType.INPUT) private Long id;
private Long orderId;
private String customerId;
private String operationType;
private String fromStatus;
private String toStatus;
private String operatorId;
private String changeDetail;
private String remark;
private LocalDateTime createdAt;
}
@@ -5,7 +5,8 @@ import com.baiyue.lastleg.procurement.entity.OverseasProcurementOrder;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
@Mapper
public interface OverseasProcurementOrderMapper extends BaseMapper<OverseasProcurementOrder> {
@@ -18,8 +19,10 @@ public interface OverseasProcurementOrderMapper extends BaseMapper<OverseasProcu
@Select("SELECT * FROM ll_overseas_procurement_order WHERE customer_id=#{customerId} AND order_no=#{orderNo} LIMIT 1 FOR UPDATE")
OverseasProcurementOrder lockNo(@Param("customerId") String customerId, @Param("orderNo") String orderNo);
@Update("UPDATE ll_overseas_procurement_order SET outbound_quantity=outbound_quantity+#{quantity},version=version+1," +
"updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND customer_id=#{customerId} " +
"AND inbound_quantity-outbound_quantity>=#{quantity}")
int addOutbound(@Param("id") Long id, @Param("customerId") String customerId, @Param("quantity") long quantity);
@Select("<script>SELECT customer_id,MAX(customer_name) AS customer_name " +
"FROM ll_overseas_procurement_order " +
"<if test='keyword != null and keyword != &quot;&quot;'>" +
"WHERE customer_id LIKE CONCAT('%',#{keyword},'%') OR customer_name LIKE CONCAT('%',#{keyword},'%')" +
"</if> GROUP BY customer_id ORDER BY MAX(customer_name),customer_id LIMIT #{limit}</script>")
List<OverseasProcurementOrder> customerOptions(@Param("keyword") String keyword, @Param("limit") int limit);
}
@@ -1,16 +0,0 @@
package com.baiyue.lastleg.procurement.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.procurement.entity.OverseasProcurementOutbound;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface OverseasProcurementOutboundMapper extends BaseMapper<OverseasProcurementOutbound> {
@Select("SELECT * FROM ll_overseas_procurement_outbound WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY outbound_at DESC,id DESC")
List<OverseasProcurementOutbound> listOrder(@Param("customerId") String customerId,
@Param("orderId") Long orderId);
}
@@ -4,15 +4,16 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.lastleg.masterdata.quote.service.PartnerWarehouseDirectory;
import com.baiyue.lastleg.procurement.dto.OverseasProcurementRequests;
import com.baiyue.lastleg.procurement.dto.OverseasProcurementResponses;
import com.baiyue.lastleg.procurement.entity.OverseasProcurementOrder;
import com.baiyue.lastleg.procurement.entity.OverseasProcurementOutbound;
import com.baiyue.lastleg.procurement.entity.OverseasProcurementStatusHistory;
import com.baiyue.lastleg.procurement.mapper.OverseasProcurementOrderMapper;
import com.baiyue.lastleg.procurement.mapper.OverseasProcurementOutboundMapper;
import com.baiyue.lastleg.procurement.mapper.OverseasProcurementStatusHistoryMapper;
import com.baiyue.lastleg.security.OverseasPermissions;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
@@ -26,21 +27,26 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.HexFormat;
import java.util.Objects;
import java.util.Set;
@Service
public class OverseasProcurementService {
public static final String REVIEW_PERMISSION = "lastleg:overseas-procurement:review";
public static final String MANAGE_PERMISSION = "lastleg:overseas-procurement:manage";
public static final String REVIEW_PERMISSION = OverseasPermissions.PURCHASE_REVIEW;
public static final String MANAGE_PERMISSION = OverseasPermissions.PURCHASE_EDIT;
public static final String PENDING_REVIEW = "PENDING_REVIEW";
public static final String APPROVED = "APPROVED";
public static final String REJECTED = "REJECTED";
private static final Set<String> STATUSES = Set.of(PENDING_REVIEW, APPROVED, REJECTED);
@Resource private OverseasProcurementOrderMapper orders;
@Resource private OverseasProcurementOutboundMapper outbounds;
@Resource private OverseasProcurementStatusHistoryMapper histories;
@Resource private PartnerWarehouseDirectory warehouseDirectory;
@Resource private ObjectMapper objectMapper;
@Transactional
@@ -68,7 +74,6 @@ public class OverseasProcurementService {
order.setContactName(request.contactName().trim());
order.setContactPhone(request.contactPhone().trim());
order.setInboundQuantity(0L);
order.setOutboundQuantity(0L);
order.setStatus(PENDING_REVIEW);
order.setRemark(trim(request.remark()));
order.setIdempotencyKey(key);
@@ -84,10 +89,25 @@ public class OverseasProcurementService {
if (concurrent != null) return replay(customerId, concurrent, requestHash);
throw exception;
}
history(order, null, PENDING_REVIEW, operatorId, request.remark(), now);
history(order, "CREATE", null, PENDING_REVIEW, operatorId, null, request.remark(), now);
return detail(customerId, order.getOrderNo());
}
public List<OverseasProcurementResponses.CustomerOption> customerOptions(String keyword, int limit) {
if (limit < 1 || limit > 200) throw badRequest("limit must be between 1 and 200");
return orders.customerOptions(trim(keyword), limit).stream()
.map(value -> new OverseasProcurementResponses.CustomerOption(value.getCustomerId(),
Objects.requireNonNullElse(trim(value.getCustomerName()), value.getCustomerId())))
.toList();
}
public List<OverseasProcurementResponses.WarehouseOption> warehouseOptions() {
return warehouseDirectory.options().stream()
.map(value -> new OverseasProcurementResponses.WarehouseOption(value.warehouseId(),
value.warehouseCode(), value.warehouseName(), value.countryCode()))
.toList();
}
public PageResult<OverseasProcurementResponses.Summary> page(
String customerId, OverseasProcurementRequests.Query query) {
validateQuery(query);
@@ -122,10 +142,10 @@ public class OverseasProcurementService {
order.getDomesticCarrier(), order.getDomesticTrackingNo(), order.getOverseasCarrier(),
order.getOverseasTrackingNo(), order.getEstimatedArrivalAt(), order.getRemark(),
order.getCreatedBy(), order.getReviewedBy(), order.getReviewRemark(), order.getReviewedAt(),
outbounds.listOrder(customerId, order.getId()).stream().map(this::outbound).toList(),
histories.listOrder(customerId, order.getId()).stream().map(value ->
new OverseasProcurementResponses.History(value.getFromStatus(), value.getToStatus(),
value.getOperatorId(), value.getRemark(), value.getCreatedAt())).toList());
new OverseasProcurementResponses.History(value.getOperationType(), value.getFromStatus(),
value.getToStatus(), value.getOperatorId(), value.getChangeDetail(), value.getRemark(),
value.getCreatedAt())).toList());
}
@Transactional
@@ -146,64 +166,38 @@ public class OverseasProcurementService {
order.setReviewedAt(now);
touch(order, now);
orders.updateById(order);
history(order, PENDING_REVIEW, next, operatorId, request.remark(), now);
history(order, "REVIEW", PENDING_REVIEW, next, operatorId, null, request.remark(), now);
return detail(customerId, orderNo);
}
@Transactional
public OverseasProcurementResponses.Detail update(String customerId, String orderNo,
public OverseasProcurementResponses.Detail update(String customerId, String orderNo, String operatorId,
OverseasProcurementRequests.Update request) {
OverseasProcurementOrder order = lock(customerId, orderNo);
if (!request.version().equals(order.getVersion())) throw conflict("Order was changed; refresh and retry");
Map<String, Object> before = editableSnapshot(order);
if (request.inboundQuantity() != null) {
if (!APPROVED.equals(order.getStatus())) throw conflict("Only an approved procurement order can receive stock");
if (request.inboundQuantity() < value(order.getOutboundQuantity())) {
throw conflict("Inbound quantity cannot be lower than the recorded outbound quantity");
}
order.setInboundQuantity(request.inboundQuantity());
}
setIfText(request.purchaseCategory(), order::setPurchaseCategory);
setIfText(request.productName(), order::setProductName);
setIfPresent(request.productSpecification(), order::setProductSpecification);
setIfPresent(request.productUrl(), order::setProductUrl);
setIfPresent(request.purchaseWarehouseId(), order::setPurchaseWarehouseId);
setIfPresent(request.purchaseWarehouseName(), order::setPurchaseWarehouseName);
setIfPresent(request.domesticCarrier(), order::setDomesticCarrier);
setIfPresent(request.domesticTrackingNo(), order::setDomesticTrackingNo);
setIfPresent(request.overseasCarrier(), order::setOverseasCarrier);
setIfPresent(request.overseasTrackingNo(), order::setOverseasTrackingNo);
if (request.estimatedArrivalAt() != null) order.setEstimatedArrivalAt(request.estimatedArrivalAt());
setIfPresent(request.remark(), order::setRemark);
touch(order, LocalDateTime.now());
patchText(request.productSpecification(), 500, "productSpecification", order::setProductSpecification);
patchText(request.productUrl(), 2000, "productUrl", order::setProductUrl);
patchWarehouse(order, request.purchaseWarehouseId(), request.purchaseWarehouseName());
patchText(request.domesticCarrier(), 128, "domesticCarrier", order::setDomesticCarrier);
patchText(request.domesticTrackingNo(), 128, "domesticTrackingNo", order::setDomesticTrackingNo);
patchText(request.overseasCarrier(), 128, "overseasCarrier", order::setOverseasCarrier);
patchText(request.overseasTrackingNo(), 128, "overseasTrackingNo", order::setOverseasTrackingNo);
patchDateTime(request.estimatedArrivalAt(), order::setEstimatedArrivalAt);
patchText(request.remark(), 1000, "remark", order::setRemark);
Map<String, Object> after = editableSnapshot(order);
if (before.equals(after)) return detail(customerId, orderNo);
LocalDateTime now = LocalDateTime.now();
touch(order, now);
orders.updateById(order);
return detail(customerId, orderNo);
}
@Transactional
public OverseasProcurementResponses.Detail addOutbound(String customerId, String orderNo, String operatorId,
OverseasProcurementRequests.AddOutbound request) {
OverseasProcurementOrder order = lock(customerId, orderNo);
if (!APPROVED.equals(order.getStatus())) throw conflict("Only an approved procurement order can ship stock");
if (orders.addOutbound(order.getId(), customerId, request.quantity()) != 1) {
throw conflict("Outbound quantity exceeds current procurement inventory");
}
OverseasProcurementOutbound outbound = new OverseasProcurementOutbound();
outbound.setId(IdWorker.getId());
outbound.setOrderId(order.getId());
outbound.setCustomerId(customerId);
outbound.setQuantity(request.quantity());
outbound.setOutboundNo(request.outboundNo().trim());
outbound.setCarrier(trim(request.carrier()));
outbound.setTrackingNo(trim(request.trackingNo()));
outbound.setOutboundAt(request.outboundAt() == null ? LocalDateTime.now() : request.outboundAt());
outbound.setRemark(trim(request.remark()));
outbound.setCreatedBy(operatorId);
outbound.setCreatedAt(LocalDateTime.now());
try {
outbounds.insert(outbound);
} catch (DuplicateKeyException exception) {
throw conflict("Outbound number already exists for this procurement order");
}
history(order, "UPDATE", order.getStatus(), order.getStatus(), operatorId,
changeDetail(before, after), null, now);
return detail(customerId, orderNo);
}
@@ -222,29 +216,24 @@ public class OverseasProcurementService {
private OverseasProcurementResponses.Summary summary(OverseasProcurementOrder order) {
long inbound = value(order.getInboundQuantity());
long outbound = value(order.getOutboundQuantity());
return new OverseasProcurementResponses.Summary(order.getId(), order.getOrderNo(), order.getCustomerId(),
order.getCustomerName(), order.getPurchaseCategory(), order.getProductName(),
order.getProductSpecification(), value(order.getPurchaseQuantity()), order.getBudgetUnitPrice(),
order.getCurrency(), order.getPurchaseWarehouseId(), order.getPurchaseWarehouseName(), inbound, outbound,
inbound - outbound, order.getStatus(), value(order.getVersion()), order.getCreatedAt(), order.getUpdatedAt());
order.getCurrency(), order.getPurchaseWarehouseId(), order.getPurchaseWarehouseName(), inbound, inbound,
order.getStatus(), value(order.getVersion()), order.getCreatedAt(), order.getUpdatedAt());
}
private OverseasProcurementResponses.Outbound outbound(OverseasProcurementOutbound value) {
return new OverseasProcurementResponses.Outbound(value.getId(), value(value.getQuantity()), value.getOutboundNo(),
value.getCarrier(), value.getTrackingNo(), value.getOutboundAt(), value.getRemark(), value.getCreatedBy(),
value.getCreatedAt());
}
private void history(OverseasProcurementOrder order, String from, String to, String operator,
String remark, LocalDateTime now) {
private void history(OverseasProcurementOrder order, String operationType, String from, String to,
String operator, String changeDetail, String remark, LocalDateTime now) {
OverseasProcurementStatusHistory history = new OverseasProcurementStatusHistory();
history.setId(IdWorker.getId());
history.setOrderId(order.getId());
history.setCustomerId(order.getCustomerId());
history.setOperationType(operationType);
history.setFromStatus(from);
history.setToStatus(to);
history.setOperatorId(operator);
history.setChangeDetail(changeDetail);
history.setRemark(trim(remark));
history.setCreatedAt(now);
histories.insert(history);
@@ -267,6 +256,91 @@ public class OverseasProcurementService {
}
}
private void patchWarehouse(OverseasProcurementOrder order, JsonNode warehouseId, JsonNode warehouseName) {
if (warehouseId == null) {
if (warehouseName != null) throw badRequest("purchaseWarehouseName cannot be changed without purchaseWarehouseId");
return;
}
String id = nullableText(warehouseId, 128, "purchaseWarehouseId");
if (id == null) {
order.setPurchaseWarehouseId(null);
order.setPurchaseWarehouseName(null);
return;
}
long numericId;
try {
numericId = Long.parseLong(id);
} catch (NumberFormatException exception) {
throw badRequest("purchaseWarehouseId must be a numeric overseas partner warehouse ID");
}
var option = warehouseDirectory.required(numericId);
order.setPurchaseWarehouseId(option.warehouseId());
order.setPurchaseWarehouseName(option.warehouseName());
}
private static void patchText(JsonNode node, int maxLength, String field,
java.util.function.Consumer<String> setter) {
if (node != null) setter.accept(nullableText(node, maxLength, field));
}
private static String nullableText(JsonNode node, int maxLength, String field) {
if (node.isNull()) return null;
if (!node.isTextual()) throw badRequest(field + " must be a string or null");
String value = trim(node.textValue());
if (value != null && value.length() > maxLength) throw badRequest(field + " exceeds max length " + maxLength);
return value;
}
private static void patchDateTime(JsonNode node, java.util.function.Consumer<LocalDateTime> setter) {
if (node == null) return;
String value = nullableText(node, 64, "estimatedArrivalAt");
if (value == null) {
setter.accept(null);
return;
}
try {
setter.accept(LocalDateTime.parse(value));
} catch (DateTimeParseException exception) {
throw badRequest("estimatedArrivalAt must be an ISO local date-time or null");
}
}
private static Map<String, Object> editableSnapshot(OverseasProcurementOrder order) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("purchaseCategory", order.getPurchaseCategory());
result.put("productName", order.getProductName());
result.put("productSpecification", order.getProductSpecification());
result.put("productUrl", order.getProductUrl());
result.put("inboundQuantity", value(order.getInboundQuantity()));
result.put("purchaseWarehouseId", order.getPurchaseWarehouseId());
result.put("purchaseWarehouseName", order.getPurchaseWarehouseName());
result.put("domesticCarrier", order.getDomesticCarrier());
result.put("domesticTrackingNo", order.getDomesticTrackingNo());
result.put("overseasCarrier", order.getOverseasCarrier());
result.put("overseasTrackingNo", order.getOverseasTrackingNo());
result.put("estimatedArrivalAt", order.getEstimatedArrivalAt());
result.put("remark", order.getRemark());
return result;
}
private String changeDetail(Map<String, Object> before, Map<String, Object> after) {
Map<String, Object> changes = new LinkedHashMap<>();
before.forEach((field, oldValue) -> {
Object newValue = after.get(field);
if (!Objects.equals(oldValue, newValue)) {
Map<String, Object> pair = new LinkedHashMap<>();
pair.put("before", oldValue);
pair.put("after", newValue);
changes.put(field, pair);
}
});
try {
return objectMapper.writeValueAsString(changes);
} catch (JsonProcessingException exception) {
throw new IllegalStateException("Cannot serialize procurement change detail", exception);
}
}
private static void touch(OverseasProcurementOrder order, LocalDateTime now) {
order.setVersion(value(order.getVersion()) + 1);
order.setUpdatedAt(now);
@@ -278,9 +352,6 @@ public class OverseasProcurementService {
private static void setIfText(String value, java.util.function.Consumer<String> setter) {
if (StringUtils.hasText(value)) setter.accept(value.trim());
}
private static void setIfPresent(String value, java.util.function.Consumer<String> setter) {
if (value != null) setter.accept(trim(value));
}
private static ResponseStatusException badRequest(String message) {
return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
}