fix(customs): add broker submission and receipt action history
This commit is contained in:
+7
@@ -84,6 +84,13 @@ public class MerchantContainerCustomsController {
|
||||
return ApiResponse.success(service.deleteSource(tenantId,documentId,user.userId(),key,expectedVersion));
|
||||
}
|
||||
|
||||
@GetMapping("/documents/{documentId}/receipt/actions")
|
||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||
public ApiResponse<List<ReceiptActionView>> receiptActions(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long documentId) {
|
||||
return ApiResponse.success(service.receiptActions(FirstlegScopeGuard.merchant(user),documentId,false));
|
||||
}
|
||||
|
||||
@GetMapping("/documents/{documentId}/download")
|
||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||
public ResponseEntity<byte[]> downloadSource(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+19
@@ -105,6 +105,17 @@ public class PlatformContainerCustomsController {
|
||||
return ApiResponse.success(service.start(planId,user.userId(),user.username(),key,request));
|
||||
}
|
||||
|
||||
@PostMapping("/{planId}/send-to-broker")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@BusinessAudit(event="CUSTOMS_INITIAL_SEND_TO_BROKER",resourceType="CUSTOMS_CONTAINER")
|
||||
public ApiResponse<ContainerView> sendInitialToBroker(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long planId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ActionRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.sendInitialToBroker(planId,user.userId(),user.username(),key,request));
|
||||
}
|
||||
|
||||
@PostMapping("/{planId}/submit-to-broker")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@BusinessAudit(event="CUSTOMS_SUBMIT_TO_BROKER",resourceType="CUSTOMS_CONTAINER")
|
||||
@@ -152,6 +163,14 @@ public class PlatformContainerCustomsController {
|
||||
return ApiResponse.success(service.completeAllBuy(planId,user.userId(),user.username(),key,request));
|
||||
}
|
||||
|
||||
@GetMapping("/documents/{documentId}/receipt/actions")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:list')")
|
||||
public ApiResponse<List<ReceiptActionView>> receiptActions(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long documentId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.receiptActions(0,documentId,true));
|
||||
}
|
||||
|
||||
@GetMapping("/documents/{documentId}/download")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:list')")
|
||||
public ResponseEntity<byte[]> downloadSource(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+11
-2
@@ -20,18 +20,27 @@ public final class ContainerCustomsDtos {
|
||||
public record ReceiptVersionView(long version, FileView file, String operatorName,
|
||||
LocalDateTime createdAt) {}
|
||||
|
||||
public record ReceiptActionView(long id, int receiptVersion, String action, String reason,
|
||||
String operatorName, LocalDateTime createdAt) {}
|
||||
|
||||
public record BrokerSubmissionView(long id, long customsContainerId, long loadingPlanId,
|
||||
String batchNo, String status, int sourceVersion,
|
||||
String sentByName, LocalDateTime sentAt, long version) {}
|
||||
|
||||
public record DocumentView(long documentId, long tenantId, FileView source,
|
||||
List<Long> inboundIds, List<String> inboundCodes,
|
||||
boolean frozen, FileView receipt, String receiptStatus,
|
||||
String receiptRejectReason, FileView finalDocument,
|
||||
List<ReceiptVersionView> receiptHistory) {}
|
||||
List<ReceiptVersionView> receiptHistory,
|
||||
List<ReceiptActionView> confirmationHistory) {}
|
||||
|
||||
public record ContainerView(long aggregateId, long loadingPlanId, long containerBookingId,
|
||||
String planNo, String cabinetNo, String containerNo, String status, boolean hasActualCargo,
|
||||
String merchantStatus,
|
||||
int version, int taxRefundCount, int coveredTaxRefundCount, long actualBoxCount,
|
||||
List<InboundView> inbounds, List<DocumentView> documents,
|
||||
LocalDateTime startedAt, LocalDateTime completedAt, LocalDateTime brokerSubmittedAt) {}
|
||||
LocalDateTime startedAt, LocalDateTime completedAt, LocalDateTime brokerSubmittedAt,
|
||||
BrokerSubmissionView brokerSubmission) {}
|
||||
|
||||
public record ScopeRequest(@NotEmpty @Size(max = 200) List<@Positive Long> inboundIds,
|
||||
@Min(0) int expectedVersion) {}
|
||||
|
||||
+56
@@ -119,6 +119,32 @@ public interface ContainerCustomsMapper {
|
||||
@Select("SELECT broker_submitted_at FROM fl_customs_container WHERE id=#{id}")
|
||||
LocalDateTime brokerSubmittedAt(@Param("id") long id);
|
||||
|
||||
@Select("SELECT " + BROKER_SUBMISSION_COLUMNS + " FROM fl_customs_broker_submission s "
|
||||
+ "WHERE s.customs_container_id=#{containerId}")
|
||||
BrokerSubmissionRow brokerSubmission(@Param("containerId") long containerId);
|
||||
|
||||
@Select("SELECT " + BROKER_SUBMISSION_COLUMNS + " FROM fl_customs_broker_submission s "
|
||||
+ "WHERE s.customs_container_id=#{containerId} FOR UPDATE")
|
||||
BrokerSubmissionRow lockBrokerSubmission(@Param("containerId") long containerId);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_customs_broker_submission
|
||||
(id,customs_container_id,loading_plan_id,batch_no,status,source_version,source_snapshot_json,
|
||||
sent_by,sent_by_name,sent_at,version,created_at,updated_at)
|
||||
VALUES(#{id},#{containerId},#{planId},#{batchNo},'SENT',#{sourceVersion},CAST(#{snapshotJson} AS JSON),
|
||||
#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insertBrokerSubmission(@Param("id") long id,@Param("containerId") long containerId,
|
||||
@Param("planId") long planId,@Param("batchNo") String batchNo,
|
||||
@Param("sourceVersion") int sourceVersion,@Param("snapshotJson") String snapshotJson,
|
||||
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_customs_container SET version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{id} AND version=#{version} AND status='DECLARING'
|
||||
""")
|
||||
int bumpVersionAfterBrokerSubmission(@Param("id") long id,@Param("version") int version);
|
||||
|
||||
@Select("SELECT " + DECLARATION_BATCH_COLUMNS + " FROM fl_customs_declaration_batch b "
|
||||
+ "WHERE customs_container_id=#{containerId} ORDER BY created_at DESC,id DESC")
|
||||
List<DeclarationBatchRow> declarationBatches(@Param("containerId") long containerId);
|
||||
@@ -387,6 +413,30 @@ public interface ContainerCustomsMapper {
|
||||
@Param("status") String status,@Param("reason") String reason,
|
||||
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_customs_completion_action_history
|
||||
(id,completion_document_id,receipt_version,action,reason,operator_id,operator_name)
|
||||
VALUES(#{id},#{completionId},#{receiptVersion},#{action},#{reason},#{operatorId},#{operatorName})
|
||||
""")
|
||||
int insertReceiptAction(@Param("id") long id,@Param("completionId") long completionId,
|
||||
@Param("receiptVersion") int receiptVersion,@Param("action") String action,
|
||||
@Param("reason") String reason,@Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT h.id id,h.receipt_version receiptVersion,h.action action,h.reason reason,
|
||||
h.operator_name operatorName,h.created_at createdAt
|
||||
FROM fl_customs_completion_action_history h
|
||||
JOIN fl_customs_completion_document r ON r.id=h.completion_document_id
|
||||
JOIN fl_customs_customer_document d ON d.id=r.source_document_id
|
||||
WHERE d.id=#{documentId} AND d.status='ACTIVE'
|
||||
<if test="tenantId != null">AND d.tenant_id=#{tenantId}</if>
|
||||
ORDER BY h.created_at,h.id
|
||||
</script>
|
||||
""")
|
||||
List<ReceiptActionRow> receiptActionHistory(@Param("documentId") long documentId,@Param("tenantId") Long tenantId);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*) FROM fl_customs_customer_document d
|
||||
JOIN fl_customs_completion_document r ON r.source_document_id=d.id
|
||||
@@ -561,6 +611,8 @@ public interface ContainerCustomsMapper {
|
||||
String customerConfirmStatus,String customerRejectReason) {}
|
||||
record CompletionVersionRow(Integer documentVersion,Long fileId,String fileName,String fileHash,String contentType,
|
||||
Long sizeBytes,Long operatorId,String operatorName,LocalDateTime createdAt) {}
|
||||
record ReceiptActionRow(Long id,Integer receiptVersion,String action,String reason,
|
||||
String operatorName,LocalDateTime createdAt) {}
|
||||
record FinalRow(Long id,Long sourceDocumentId,Long fileId,String fileName,String fileHash,String contentType,
|
||||
Long sizeBytes,Long uploadedBy,String uploadedByName,LocalDateTime uploadedAt,Integer version) {}
|
||||
record CommandRow(Long id,Long actorId,String action,String idempotencyKey,String requestHash,String status,
|
||||
@@ -568,6 +620,8 @@ public interface ContainerCustomsMapper {
|
||||
String DECLARATION_BATCH_COLUMNS = "b.id id,b.customs_container_id customsContainerId,b.loading_plan_id loadingPlanId,"+
|
||||
"b.batch_no batchNo,b.status status,b.source_version sourceVersion,b.customs_entry_no customsEntryNo,b.submitted_by_name submittedByName,"+
|
||||
"b.submitted_at submittedAt,b.acknowledged_by_name acknowledgedByName,b.acknowledged_at acknowledgedAt,b.remark remark,b.version version,b.created_at createdAt,b.updated_at updatedAt";
|
||||
String BROKER_SUBMISSION_COLUMNS = "s.id id,s.customs_container_id customsContainerId,s.loading_plan_id loadingPlanId,"+
|
||||
"s.batch_no batchNo,s.status status,s.source_version sourceVersion,s.sent_by_name sentByName,s.sent_at sentAt,s.version version";
|
||||
String CLEARANCE_RESULT_COLUMNS = "r.id id,r.declaration_batch_id declarationBatchId,r.customs_container_id customsContainerId,"+
|
||||
"r.result_type resultType,r.result_status resultStatus,r.customs_entry_no customsEntryNo,r.occurred_at occurredAt,"+
|
||||
"r.resolution resolution,r.evidence_file_id evidenceFileId,r.recorded_by recordedBy,r.recorded_by_name recordedByName,"+
|
||||
@@ -576,6 +630,8 @@ public interface ContainerCustomsMapper {
|
||||
Integer sourceVersion,String customsEntryNo,String submittedByName,LocalDateTime submittedAt,
|
||||
String acknowledgedByName,LocalDateTime acknowledgedAt,String remark,Integer version,
|
||||
LocalDateTime createdAt,LocalDateTime updatedAt) {}
|
||||
record BrokerSubmissionRow(Long id,Long customsContainerId,Long loadingPlanId,String batchNo,String status,
|
||||
Integer sourceVersion,String sentByName,LocalDateTime sentAt,Integer version) {}
|
||||
record ClearanceResultRow(Long id,Long declarationBatchId,Long customsContainerId,String resultType,String resultStatus,
|
||||
String customsEntryNo,LocalDateTime occurredAt,String resolution,Long evidenceFileId,
|
||||
Long recordedBy,String recordedByName,LocalDateTime recordedAt,Long previousResultId) {}
|
||||
|
||||
+79
-2
@@ -21,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
@@ -173,6 +174,35 @@ public class ContainerCustomsService {
|
||||
return platformView(planId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerView sendInitialToBroker(long planId, long operatorId, String operatorName, String key,
|
||||
ActionRequest request) {
|
||||
String action = "SEND_CUSTOMS_INITIAL_TO_BROKER_" + planId;
|
||||
if (!request.confirm()) throw bad("请确认已将冻结后的报关资料发送给报关行");
|
||||
String requestHash = hash(action, request.expectedVersion(), request.confirm(), request.remark());
|
||||
CommandRow replay = claim(operatorId, action, key, requestHash);
|
||||
if (replay != null) return platformView(planId);
|
||||
ContainerRow container = mapper.lockContainer(planId);
|
||||
if (container == null || !DECLARING.equals(container.status())) throw conflict("当前柜不在报关中");
|
||||
if (!Objects.equals(container.version(), request.expectedVersion())) throw concurrent();
|
||||
if (mapper.taxRefundCount(planId) == 0) throw conflict("全买单柜无需发送报关资料给报关行");
|
||||
if (mapper.brokerSubmission(container.id()) != null) throw conflict("当前柜已完成首次发送报关行批次");
|
||||
List<DocumentRow> documents = mapper.documents(container.id(), null);
|
||||
if (documents.isEmpty() || documents.stream().anyMatch(document -> document.frozenAt() == null)) {
|
||||
throw conflict("报关资料尚未全部冻结,不能发送给报关行");
|
||||
}
|
||||
long submissionId = IdWorker.getId();
|
||||
String batchNo = brokerBatchNo(planId);
|
||||
String snapshot = brokerSubmissionSnapshot(container.id());
|
||||
if (mapper.insertBrokerSubmission(submissionId, container.id(), planId, batchNo, container.version(), snapshot,
|
||||
operatorId, operatorName) != 1) throw conflict("首次发送报关行批次创建失败");
|
||||
if (mapper.bumpVersionAfterBrokerSubmission(container.id(), container.version()) != 1) throw concurrent();
|
||||
history(container, "SEND_CUSTOMS_INITIAL_TO_BROKER", container.status(), container.status(),
|
||||
request.remark(), operatorId, operatorName);
|
||||
completeCommand(operatorId, action, key, submissionId);
|
||||
return platformView(planId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerView submitToBroker(long planId, long operatorId, String operatorName, String key, ActionRequest request) {
|
||||
String action = "SUBMIT_CUSTOMS_TO_BROKER_" + planId;
|
||||
@@ -185,6 +215,7 @@ public class ContainerCustomsService {
|
||||
if (!Objects.equals(container.version(), request.expectedVersion())) throw concurrent();
|
||||
if (mapper.taxRefundCount(container.loadingPlanId()) == 0) throw conflict("全买单柜无需发送客户回执给报关行");
|
||||
if (mapper.unconfirmedReceiptCount(container.id()) != 0) throw conflict("仍有报关行回执未完成客户确认");
|
||||
if (mapper.brokerSubmission(container.id()) == null) throw conflict("当前柜尚未完成首次发送报关行资料");
|
||||
if (mapper.brokerSubmittedAt(container.id()) != null) throw conflict("当前柜已发送给报关行执行");
|
||||
if (mapper.submitToBroker(container.id(), container.version(), operatorId, operatorName) != 1) throw concurrent();
|
||||
history(container, "SUBMIT_CUSTOMS_TO_BROKER", container.status(), container.status(), request.remark(), operatorId, operatorName);
|
||||
@@ -209,6 +240,9 @@ public class ContainerCustomsService {
|
||||
if (mapper.brokerSubmittedAt(container.id()) != null) {
|
||||
throw conflict("客户确认回执已发送给报关行,不可再修改回执");
|
||||
}
|
||||
if (mapper.brokerSubmission(container.id()) == null) {
|
||||
throw conflict("当前柜尚未完成首次发送报关行资料");
|
||||
}
|
||||
CompletionRow completion = mapper.lockCompletion(documentId);
|
||||
CustomsFileClient.Metadata metadata = upload(source.tenantId(), RECEIPT_FILE_TYPE, Long.toString(container.loadingPlanId()), operatorId, file);
|
||||
long completionId;
|
||||
@@ -249,6 +283,8 @@ public class ContainerCustomsService {
|
||||
if (!confirm && (reason == null || reason.isBlank())) throw bad("打回回执必须填写原因");
|
||||
if (mapper.confirmReceipt(documentId, tenantId, confirm ? "CONFIRMED" : "REJECTED",
|
||||
confirm ? null : trim(reason), operatorId, operatorName) != 1) throw conflict("回执状态已变化,请刷新后重试");
|
||||
if (mapper.insertReceiptAction(IdWorker.getId(), receipt.id(), receipt.version(), confirm ? "CONFIRM" : "REJECT",
|
||||
trim(reason), operatorId, operatorName) != 1) throw concurrent();
|
||||
if (confirm && mapper.pendingReceiptConfirmationCount(container.id()) == 0) {
|
||||
history(container, "ALL_CUSTOMS_RECEIPTS_CONFIRMED", container.status(), container.status(), "客户已确认全部报关行回执,待发送给报关行执行", operatorId, operatorName);
|
||||
}
|
||||
@@ -439,6 +475,12 @@ public class ContainerCustomsService {
|
||||
row.recordedBy() == null ? 0 : row.recordedBy(), row.recordedByName(), row.recordedAt(), row.previousResultId());
|
||||
}
|
||||
|
||||
private BrokerSubmissionView brokerSubmissionView(BrokerSubmissionRow row) {
|
||||
return new BrokerSubmissionView(row.id(), row.customsContainerId(), row.loadingPlanId(), row.batchNo(),
|
||||
row.status(), row.sourceVersion() == null ? 0 : row.sourceVersion(), row.sentByName(), row.sentAt(),
|
||||
row.version() == null ? 0 : row.version());
|
||||
}
|
||||
|
||||
private String declarationSnapshot(long containerId) {
|
||||
List<Map<String,Object>> snapshot = mapper.documents(containerId, null).stream().map(doc -> {
|
||||
CompletionRow receipt = mapper.completion(doc.id());
|
||||
@@ -451,6 +493,26 @@ public class ContainerCustomsService {
|
||||
catch (JsonProcessingException ex) { throw new IllegalStateException(ex); }
|
||||
}
|
||||
|
||||
private String brokerBatchNo(long planId) {
|
||||
PlanRow plan = requiredPlan(planId);
|
||||
String planNo = trim(plan.planNo());
|
||||
if (planNo != null && planNo.matches("[A-Za-z0-9_-]{1,52}")) return "BR-" + planNo + "-01";
|
||||
String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase(Locale.ROOT);
|
||||
return "BR-" + LocalDate.now() + "-" + suffix;
|
||||
}
|
||||
|
||||
private String brokerSubmissionSnapshot(long containerId) {
|
||||
List<Map<String,Object>> snapshot = mapper.documents(containerId, null).stream().map(document -> {
|
||||
List<ScopeRow> scopes = mapper.frozenScopes(document.id());
|
||||
return Map.<String,Object>of("documentId", document.id(), "tenantId", document.tenantId(),
|
||||
"sourceFileId", document.fileId(), "sourceFileName", document.fileName(), "sourceFileHash", document.fileHash(),
|
||||
"frozenVersion", document.frozenVersion(), "inboundIds", scopes.stream().map(ScopeRow::inboundId).toList(),
|
||||
"inboundCodes", scopes.stream().map(ScopeRow::inboundCode).toList());
|
||||
}).toList();
|
||||
try { return objectMapper.writeValueAsString(snapshot); }
|
||||
catch (JsonProcessingException ex) { throw new IllegalStateException(ex); }
|
||||
}
|
||||
|
||||
public Download downloadSource(long tenantId, long documentId, boolean platform) {
|
||||
DocumentRow source = requiredDocument(documentId);
|
||||
if (!platform && source.tenantId() != tenantId) throw notFound("客户报送资料不存在");
|
||||
@@ -458,6 +520,18 @@ public class ContainerCustomsService {
|
||||
return download(source.tenantId(), SOURCE_FILE_TYPE, Long.toString(container.loadingPlanId()), source.fileId(), source.fileName());
|
||||
}
|
||||
|
||||
public List<ReceiptActionView> receiptActions(long tenantId, long documentId, boolean platform) {
|
||||
DocumentRow source = requiredDocument(documentId);
|
||||
if (!platform && source.tenantId() != tenantId) throw notFound("客户报送资料不存在");
|
||||
return receiptActionViews(documentId, platform ? null : tenantId);
|
||||
}
|
||||
|
||||
private List<ReceiptActionView> receiptActionViews(long documentId, Long tenantId) {
|
||||
return Optional.ofNullable(mapper.receiptActionHistory(documentId, tenantId)).orElseGet(List::of).stream()
|
||||
.map(row -> new ReceiptActionView(row.id(), row.receiptVersion(), row.action(), row.reason(),
|
||||
row.operatorName(), row.createdAt())).toList();
|
||||
}
|
||||
|
||||
public Download downloadCompletion(long tenantId, long documentId, boolean platform) {
|
||||
DocumentRow source = requiredDocument(documentId);
|
||||
ContainerRow container = mapper.containerById(source.customsContainerId());
|
||||
@@ -570,9 +644,11 @@ public class ContainerCustomsService {
|
||||
long actualBoxCount = mapper.actualBoxCount(container.loadingPlanId(), tenantId,
|
||||
tenantId == null ? null : "TAX_REFUND");
|
||||
String merchantStatus=tenantId==null?null:merchantStatus(container.status(),refund,coveredRefund);
|
||||
BrokerSubmissionRow submission = mapper.brokerSubmission(container.id());
|
||||
return new ContainerView(container.id(), container.loadingPlanId(), container.containerBookingId(), plan.planNo(),
|
||||
plan.cabinetNo(), plan.containerNo(), container.status(), !rows.isEmpty(), merchantStatus, container.version(), refund, coveredRefund,
|
||||
actualBoxCount, inbounds, documentViews, container.startedAt(), container.completedAt(), container.brokerSubmittedAt());
|
||||
actualBoxCount, inbounds, documentViews, container.startedAt(), container.completedAt(), container.brokerSubmittedAt(),
|
||||
submission == null ? null : brokerSubmissionView(submission));
|
||||
}
|
||||
|
||||
private DocumentView documentView(DocumentRow source, boolean merchant) {
|
||||
@@ -587,7 +663,8 @@ public class ContainerCustomsService {
|
||||
scopes.stream().map(ScopeRow::inboundCode).toList(), source.frozenAt() != null, result,
|
||||
completion == null ? null : completion.customerConfirmStatus(),
|
||||
completion == null ? null : completion.customerRejectReason(),
|
||||
finalDocument == null ? null : file(finalDocument), receiptHistory);
|
||||
finalDocument == null ? null : file(finalDocument), receiptHistory,
|
||||
receiptActionViews(source.id(), merchant ? source.tenantId() : null));
|
||||
}
|
||||
|
||||
private void completeContainer(ContainerRow container, long operatorId, String operatorName, String remark) {
|
||||
|
||||
Reference in New Issue
Block a user