完善退税报关回执确认流程
This commit is contained in:
+20
@@ -91,6 +91,26 @@ public class MerchantContainerCustomsController {
|
||||
return response(service.downloadCompletion(FirstlegScopeGuard.merchant(user),documentId,false));
|
||||
}
|
||||
|
||||
@PostMapping("/documents/{documentId}/receipt/confirm")
|
||||
@PreAuthorize("hasAuthority('merchant:order:edit')")
|
||||
public ApiResponse<ContainerView> confirmReceipt(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long documentId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ActionRequest request) {
|
||||
long tenantId=FirstlegScopeGuard.merchant(user);
|
||||
return ApiResponse.success(service.confirmReceipt(tenantId,documentId,user.userId(),user.username(),key,request.expectedVersion(),true,request.remark()));
|
||||
}
|
||||
|
||||
@PostMapping("/documents/{documentId}/receipt/reject")
|
||||
@PreAuthorize("hasAuthority('merchant:order:edit')")
|
||||
public ApiResponse<ContainerView> rejectReceipt(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long documentId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ActionRequest request) {
|
||||
long tenantId=FirstlegScopeGuard.merchant(user);
|
||||
return ApiResponse.success(service.confirmReceipt(tenantId,documentId,user.userId(),user.username(),key,request.expectedVersion(),false,request.remark()));
|
||||
}
|
||||
|
||||
private static ResponseEntity<byte[]> response(Download value) {
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.contentType(MediaType.parseMediaType(value.contentType()))
|
||||
|
||||
+1
-2
@@ -51,11 +51,10 @@ public class PlatformContainerCustomsController {
|
||||
@PathVariable @Positive long documentId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@RequestParam @Min(0) int expectedVersion,
|
||||
@RequestParam(defaultValue="false") boolean finalConfirmation,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.uploadCompletion(documentId,user.userId(),user.username(),key,
|
||||
expectedVersion,finalConfirmation,file));
|
||||
expectedVersion,file));
|
||||
}
|
||||
|
||||
@PostMapping("/{planId}/complete-all-buy")
|
||||
|
||||
+3
-2
@@ -19,12 +19,13 @@ public final class ContainerCustomsDtos {
|
||||
|
||||
public record DocumentView(long documentId, long tenantId, FileView source,
|
||||
List<Long> inboundIds, List<String> inboundCodes,
|
||||
boolean frozen, FileView completion) {}
|
||||
boolean frozen, FileView completion, String receiptStatus,
|
||||
String receiptRejectReason) {}
|
||||
|
||||
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,
|
||||
int version, int taxRefundCount, int coveredTaxRefundCount, long actualBoxCount,
|
||||
List<InboundView> inbounds, List<DocumentView> documents,
|
||||
LocalDateTime startedAt, LocalDateTime completedAt) {}
|
||||
|
||||
|
||||
+43
-12
@@ -17,7 +17,8 @@ public interface ContainerCustomsMapper {
|
||||
String COMPLETION_COLUMNS = """
|
||||
r.id id,r.source_document_id sourceDocumentId,r.file_id fileId,r.file_name fileName,
|
||||
r.file_hash fileHash,r.content_type contentType,r.size_bytes sizeBytes,r.uploaded_by uploadedBy,
|
||||
r.uploaded_by_name uploadedByName,r.uploaded_at uploadedAt,r.version version
|
||||
r.uploaded_by_name uploadedByName,r.uploaded_at uploadedAt,r.version version,
|
||||
r.customer_confirm_status customerConfirmStatus,r.customer_reject_reason customerRejectReason
|
||||
""";
|
||||
|
||||
@Select("""
|
||||
@@ -34,6 +35,18 @@ public interface ContainerCustomsMapper {
|
||||
""")
|
||||
List<InboundRow> planInbounds(@Param("planId") long planId);
|
||||
|
||||
@Select("""
|
||||
SELECT COALESCE(SUM(item.package_count),0)
|
||||
FROM fl_loading_plan_item item
|
||||
JOIN fl_loading_plan p ON p.id=item.loading_plan_id AND p.plan_status='COMPLETED'
|
||||
JOIN fl_inbound_order i ON i.id=item.inbound_id AND i.tenant_id=item.account_id AND i.deleted=0
|
||||
WHERE item.loading_plan_id=#{planId} AND item.deleted=0
|
||||
AND (#{tenantId} IS NULL OR i.tenant_id=#{tenantId})
|
||||
AND (#{taxMode} IS NULL OR i.tax_mode=#{taxMode})
|
||||
""")
|
||||
long actualBoxCount(@Param("planId") long planId, @Param("tenantId") Long tenantId,
|
||||
@Param("taxMode") String taxMode);
|
||||
|
||||
@Select("""
|
||||
SELECT p.id loadingPlanId,p.plan_no planNo,p.container_booking_id containerBookingId,
|
||||
p.plan_status loadingPlanStatus,b.cabinet_no cabinetNo,b.container_no containerNo
|
||||
@@ -217,9 +230,9 @@ public interface ContainerCustomsMapper {
|
||||
@Insert("""
|
||||
INSERT INTO fl_customs_completion_document
|
||||
(id,source_document_id,file_id,file_name,file_hash,content_type,size_bytes,uploaded_by,uploaded_by_name,
|
||||
uploaded_at,version,created_at,updated_at)
|
||||
uploaded_at,version,customer_confirm_status,created_at,updated_at)
|
||||
VALUES(#{id},#{documentId},#{fileId},#{fileName},#{fileHash},#{contentType},#{sizeBytes},#{operatorId},#{operatorName},
|
||||
CURRENT_TIMESTAMP(3),1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
|
||||
CURRENT_TIMESTAMP(3),1,'PENDING',CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insertCompletion(@Param("id") long id,@Param("documentId") long documentId,@Param("fileId") long fileId,
|
||||
@Param("fileName") String fileName,@Param("fileHash") String fileHash,
|
||||
@@ -229,7 +242,9 @@ public interface ContainerCustomsMapper {
|
||||
@Update("""
|
||||
UPDATE fl_customs_completion_document SET file_id=#{fileId},file_name=#{fileName},file_hash=#{fileHash},
|
||||
content_type=#{contentType},size_bytes=#{sizeBytes},uploaded_by=#{operatorId},uploaded_by_name=#{operatorName},
|
||||
uploaded_at=CURRENT_TIMESTAMP(3),updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
uploaded_at=CURRENT_TIMESTAMP(3),customer_confirm_status='PENDING',customer_confirmed_by=NULL,
|
||||
customer_confirmed_by_name=NULL,customer_confirmed_at=NULL,customer_reject_reason=NULL,
|
||||
updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE id=#{id} AND version=#{version}
|
||||
AND EXISTS(SELECT 1 FROM fl_customs_customer_document d JOIN fl_customs_container c ON c.id=d.customs_container_id
|
||||
WHERE d.id=source_document_id AND c.status='DECLARING')
|
||||
@@ -239,6 +254,28 @@ public interface ContainerCustomsMapper {
|
||||
@Param("contentType") String contentType,@Param("sizeBytes") long sizeBytes,
|
||||
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_customs_completion_document r
|
||||
JOIN fl_customs_customer_document d ON d.id=r.source_document_id
|
||||
JOIN fl_customs_container c ON c.id=d.customs_container_id
|
||||
SET r.customer_confirm_status=#{status},r.customer_confirmed_by=#{operatorId},
|
||||
r.customer_confirmed_by_name=#{operatorName},r.customer_confirmed_at=CURRENT_TIMESTAMP(3),
|
||||
r.customer_reject_reason=#{reason},r.updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE r.source_document_id=#{documentId} AND d.tenant_id=#{tenantId} AND c.status='DECLARING'
|
||||
AND r.customer_confirm_status IN ('PENDING','REJECTED')
|
||||
""")
|
||||
int confirmReceipt(@Param("documentId") long documentId,@Param("tenantId") long tenantId,
|
||||
@Param("status") String status,@Param("reason") String reason,
|
||||
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*) FROM fl_customs_customer_document d
|
||||
JOIN fl_customs_completion_document r ON r.source_document_id=d.id
|
||||
WHERE d.customs_container_id=#{containerId} AND d.status='ACTIVE' AND d.frozen_at IS NOT NULL
|
||||
AND r.customer_confirm_status <> 'CONFIRMED'
|
||||
""")
|
||||
int pendingReceiptConfirmationCount(@Param("containerId") long containerId);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_customs_completion_document_version
|
||||
(id,completion_document_id,document_version,file_id,file_name,file_hash,content_type,size_bytes,
|
||||
@@ -251,13 +288,6 @@ public interface ContainerCustomsMapper {
|
||||
@Param("contentType") String contentType,@Param("sizeBytes") long sizeBytes,
|
||||
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*) FROM fl_customs_customer_document d
|
||||
LEFT JOIN fl_customs_completion_document r ON r.source_document_id=d.id
|
||||
WHERE d.customs_container_id=#{containerId} AND d.status='ACTIVE' AND d.frozen_at IS NOT NULL AND r.id IS NULL
|
||||
""")
|
||||
int missingCompletionCount(@Param("containerId") long containerId);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_customs_container_history
|
||||
(id,customs_container_id,action,from_status,to_status,remark,operator_id,operator_name,created_at)
|
||||
@@ -350,7 +380,8 @@ public interface ContainerCustomsMapper {
|
||||
Integer frozenVersion,LocalDateTime frozenAt,Integer version) {}
|
||||
record ScopeRow(Long documentId,Long inboundId,Long orderId,String inboundCode) {}
|
||||
record CompletionRow(Long id,Long sourceDocumentId,Long fileId,String fileName,String fileHash,String contentType,
|
||||
Long sizeBytes,Long uploadedBy,String uploadedByName,LocalDateTime uploadedAt,Integer version) {}
|
||||
Long sizeBytes,Long uploadedBy,String uploadedByName,LocalDateTime uploadedAt,Integer version,
|
||||
String customerConfirmStatus,String customerRejectReason) {}
|
||||
record CommandRow(Long id,Long actorId,String action,String idempotencyKey,String requestHash,String status,
|
||||
Long resultId,LocalDateTime createdAt,LocalDateTime completedAt) {}
|
||||
record BuyExportRow(Long itemId,String planNo,String cabinetNo,String containerNo,String customerName,String orderNo,
|
||||
|
||||
+42
-12
@@ -174,11 +174,11 @@ public class ContainerCustomsService {
|
||||
|
||||
@Transactional
|
||||
public DocumentView uploadCompletion(long documentId, long operatorId, String operatorName, String key,
|
||||
int expectedVersion, boolean finalConfirmation, MultipartFile file) {
|
||||
int expectedVersion, MultipartFile file) {
|
||||
validateFile(file);
|
||||
DocumentRow readable=requiredDocument(documentId);
|
||||
String action = "UPLOAD_COMPLETION_" + documentId;
|
||||
String requestHash = hash(action, expectedVersion, finalConfirmation, fileHash(file));
|
||||
String requestHash = hash(action, expectedVersion, fileHash(file));
|
||||
CommandRow replay = claim(operatorId, action, key, requestHash);
|
||||
if (replay != null) return documentView(requiredDocument(documentId), false);
|
||||
ContainerRow container = mapper.lockContainerById(readable.customsContainerId());
|
||||
@@ -187,10 +187,6 @@ public class ContainerCustomsService {
|
||||
throw conflict("当前客户资料不在报关中冻结版本");
|
||||
}
|
||||
CompletionRow completion = mapper.lockCompletion(documentId);
|
||||
int missingBefore=mapper.missingCompletionCount(container.id());
|
||||
boolean lastNewCompletion=completion==null&&missingBefore==1;
|
||||
if(lastNewCompletion&&!finalConfirmation)throw conflict("这是最后一份完成文件,请二次确认后上传");
|
||||
if(finalConfirmation&&!lastNewCompletion)throw conflict("只有最后一份尚未上传的完成文件才能确认整柜完成");
|
||||
CustomsFileClient.Metadata metadata = upload(source.tenantId(), RESULT_FILE_TYPE, Long.toString(container.loadingPlanId()), operatorId, file);
|
||||
long completionId;
|
||||
int version;
|
||||
@@ -206,13 +202,37 @@ public class ContainerCustomsService {
|
||||
}
|
||||
mapper.insertCompletionVersion(IdWorker.getId(), completionId, version, metadata.fileId(), metadata.fileName(),
|
||||
metadata.sha256(), metadata.contentType(), size(metadata), operatorId, operatorName);
|
||||
if (lastNewCompletion) {
|
||||
completeContainer(container, operatorId, operatorName, "全部冻结客户资料已回传完成文件");
|
||||
}
|
||||
completeCommand(operatorId, action, key, completionId);
|
||||
return documentView(requiredDocument(documentId), false);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerView confirmReceipt(long tenantId, long documentId, long operatorId, String operatorName,
|
||||
String key, int expectedVersion, boolean confirm, String reason) {
|
||||
String action = (confirm ? "CONFIRM_CUSTOMS_RECEIPT_" : "REJECT_CUSTOMS_RECEIPT_") + documentId;
|
||||
String requestHash = hash(action, confirm, reason);
|
||||
CommandRow replay = claim(operatorId, action, key, requestHash);
|
||||
if (replay != null) {
|
||||
DocumentRow source = requiredDocument(documentId);
|
||||
return merchantViewForDocument(tenantId, source);
|
||||
}
|
||||
DocumentRow source = requiredDocumentLocked(documentId, tenantId);
|
||||
ContainerRow container = mapper.lockContainerById(source.customsContainerId());
|
||||
CompletionRow receipt = mapper.lockCompletion(documentId);
|
||||
if (container == null || !DECLARING.equals(container.status()) || source.frozenAt() == null || receipt == null) {
|
||||
throw conflict("当前回执不在待确认状态");
|
||||
}
|
||||
if (!Objects.equals(receipt.version(), expectedVersion)) throw concurrent();
|
||||
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 (confirm && mapper.pendingReceiptConfirmationCount(container.id()) == 0) {
|
||||
completeContainer(container, operatorId, operatorName, "客户已确认全部报关行回执");
|
||||
}
|
||||
completeCommand(operatorId, action, key, documentId);
|
||||
return merchantViewForDocument(tenantId, source);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerView completeAllBuy(long planId, long operatorId, String operatorName, String key, ActionRequest request) {
|
||||
String action = "COMPLETE_ALL_BUY_CUSTOMS_" + planId;
|
||||
@@ -336,19 +356,23 @@ public class ContainerCustomsService {
|
||||
List<DocumentView> documentViews = documents.stream().map(doc -> documentView(doc, merchant)).toList();
|
||||
int refund = (int) visibleRows.stream().filter(row -> "TAX_REFUND".equals(row.taxMode())).count();
|
||||
int coveredRefund = (int) visibleRows.stream().filter(row -> covered.contains(row.inboundId())).count();
|
||||
long actualBoxCount = mapper.actualBoxCount(container.loadingPlanId(), tenantId,
|
||||
tenantId == null ? null : "TAX_REFUND");
|
||||
String merchantStatus=tenantId==null?null:merchantStatus(container.status(),refund,coveredRefund);
|
||||
return new ContainerView(container.id(), container.loadingPlanId(), container.containerBookingId(), plan.planNo(),
|
||||
plan.cabinetNo(), plan.containerNo(), container.status(), !rows.isEmpty(), merchantStatus, container.version(), refund, coveredRefund,
|
||||
inbounds, documentViews, container.startedAt(), container.completedAt());
|
||||
actualBoxCount, inbounds, documentViews, container.startedAt(), container.completedAt());
|
||||
}
|
||||
|
||||
private DocumentView documentView(DocumentRow source, boolean merchant) {
|
||||
List<ScopeRow> scopes = source.frozenAt() == null ? mapper.scopes(source.id()) : mapper.frozenScopes(source.id());
|
||||
CompletionRow completion = mapper.completion(source.id());
|
||||
ContainerRow container = mapper.containerById(source.customsContainerId());
|
||||
FileView result = completion == null || (merchant && !COMPLETED.equals(container.status())) ? null : file(completion);
|
||||
FileView result = completion == null ? null : file(completion);
|
||||
return new DocumentView(source.id(), source.tenantId(), file(source), scopes.stream().map(ScopeRow::inboundId).toList(),
|
||||
scopes.stream().map(ScopeRow::inboundCode).toList(), source.frozenAt() != null, result);
|
||||
scopes.stream().map(ScopeRow::inboundCode).toList(), source.frozenAt() != null, result,
|
||||
completion == null ? null : completion.customerConfirmStatus(),
|
||||
completion == null ? null : completion.customerRejectReason());
|
||||
}
|
||||
|
||||
private void completeContainer(ContainerRow container, long operatorId, String operatorName, String remark) {
|
||||
@@ -406,6 +430,12 @@ public class ContainerCustomsService {
|
||||
return value;
|
||||
}
|
||||
|
||||
private ContainerView merchantViewForDocument(long tenantId, DocumentRow source) {
|
||||
List<ScopeRow> scopes = source.frozenAt() == null ? mapper.scopes(source.id()) : mapper.frozenScopes(source.id());
|
||||
if (scopes.isEmpty()) throw notFound("客户报送资料未关联入仓单");
|
||||
return merchantOrderView(tenantId, scopes.get(0).orderId());
|
||||
}
|
||||
|
||||
private String scopeJson(List<InboundRow> scope) {
|
||||
try { return objectMapper.writeValueAsString(scope.stream().map(row -> Map.of("inboundId", row.inboundId(),
|
||||
"orderId", row.orderId(), "inboundCode", row.inboundCode())).toList()); }
|
||||
|
||||
Reference in New Issue
Block a user