提交
This commit is contained in:
+4
-4
@@ -64,13 +64,13 @@ public interface MerchantWorkbenchFilterMapper {
|
||||
ORDER BY i.order_id
|
||||
""")
|
||||
List<Long> pendingCustoms(@Param("tenantId") long tenantId);
|
||||
/** Only physically loaded fragments can expose container milestones to the customer. */
|
||||
/** 仅按本客户实际装柜片段读取进度;清关资料保存后立即显示已清关。 */
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT o.order_id, CASE WHEN owned.container_booking_id IS NULL THEN 0
|
||||
WHEN EXISTS (SELECT 1 FROM fl_carrier_schedule_node_fact n
|
||||
WHERE n.container_booking_id=owned.container_booking_id
|
||||
AND n.standard_node='PICKUP_READY' AND n.actual_at IS NOT NULL) THEN 3
|
||||
WHEN EXISTS (SELECT 1 FROM fl_container_document d
|
||||
WHERE d.container_booking_id=owned.container_booking_id
|
||||
AND d.document_type='DESTINATION_CLEARANCE' AND d.file_id IS NOT NULL) THEN 3
|
||||
WHEN EXISTS (SELECT 1 FROM fl_customs_container c
|
||||
WHERE c.loading_plan_id=owned.loading_plan_id AND c.status='COMPLETED') THEN 2
|
||||
ELSE 1 END AS stage
|
||||
|
||||
@@ -17,7 +17,8 @@ public interface WorkflowFactMapper {
|
||||
+"EXISTS(SELECT 1 "+ACTUAL+" AND EXISTS(SELECT 1 FROM fl_carrier_schedule_node_fact n WHERE n.container_booking_id=b.id AND n.standard_node='DEPARTED_ORIGIN_PORT' AND n.actual_at IS NOT NULL)) departed,"
|
||||
+"EXISTS(SELECT 1 "+ACTUAL+" AND EXISTS(SELECT 1 FROM fl_container_document d WHERE d.container_booking_id=b.id AND d.document_type='DESTINATION_CLEARANCE' AND d.file_id IS NOT NULL)) clearance_uploaded,"
|
||||
+"EXISTS(SELECT 1 "+ACTUAL+" AND i.tax_mode='TAX_REFUND' AND NOT EXISTS(SELECT 1 FROM fl_customs_customer_document_inbound s JOIN fl_customs_customer_document d ON d.id=s.document_id AND d.tenant_id=i.tenant_id AND d.status='ACTIVE' JOIN fl_customs_container c ON c.id=d.customs_container_id AND c.loading_plan_id=p.id WHERE s.inbound_id=i.id AND s.tenant_id=i.tenant_id)) missing_customs,"
|
||||
+"EXISTS(SELECT 1 "+ACTUAL+" AND (i.tax_mode='BUY_DECLARATION' OR EXISTS(SELECT 1 FROM fl_customs_customer_document_inbound s JOIN fl_customs_customer_document d ON d.id=s.document_id AND d.tenant_id=i.tenant_id AND d.status='ACTIVE' JOIN fl_customs_container c ON c.id=d.customs_container_id AND c.loading_plan_id=p.id WHERE s.inbound_id=i.id AND s.tenant_id=i.tenant_id))) export_customs_ready "
|
||||
+"EXISTS(SELECT 1 "+ACTUAL+" AND (i.tax_mode='BUY_DECLARATION' OR EXISTS(SELECT 1 FROM fl_customs_customer_document_inbound s JOIN fl_customs_customer_document d ON d.id=s.document_id AND d.tenant_id=i.tenant_id AND d.status='ACTIVE' JOIN fl_customs_container c ON c.id=d.customs_container_id AND c.loading_plan_id=p.id WHERE s.inbound_id=i.id AND s.tenant_id=i.tenant_id))) export_customs_ready,"
|
||||
+"EXISTS(SELECT 1 "+ACTUAL+" AND EXISTS(SELECT 1 FROM fl_customs_container c WHERE c.loading_plan_id=p.id AND c.status='COMPLETED')) customs_completed "
|
||||
+"FROM fl_inbound_order i WHERE i.deleted=0 AND NOT EXISTS(SELECT 1 FROM fl_inbound_order child WHERE child.tenant_id=i.tenant_id AND child.parent_inbound_id=i.id AND child.inbound_role='CHILD' AND (child.split_purpose IS NULL OR child.split_purpose != 'REMAINDER') AND child.deleted=0) AND i.process_status != 'CANCELLED' AND i.id IN "
|
||||
+"<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")
|
||||
List<WorkflowFacts.Fact> facts(@Param("ids") List<Long> ids);
|
||||
|
||||
@@ -5,7 +5,7 @@ public final class WorkflowFacts {
|
||||
/** 仅承载事实查询契约,不允许实例化。 */
|
||||
private WorkflowFacts() {}
|
||||
public record Fact(long tenantId,long inboundId,long orderId,String reviewStatus,
|
||||
boolean departed,boolean clearanceUploaded,boolean missingCustoms,boolean exportCustomsReady) {}
|
||||
boolean departed,boolean clearanceUploaded,boolean missingCustoms,boolean exportCustomsReady,boolean customsCompleted) {}
|
||||
public record Source(long tenantId,long inboundId) {}
|
||||
public record Request(List<Source> sources) {}
|
||||
public record Overseas(long tenantId,long inboundId,boolean outboundExists,boolean pendingOutbound,boolean delivering,boolean unloaded) {}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class WorkflowTabService {
|
||||
if(facts.stream().noneMatch(f->f.tenantId()==fee.tenantId() && f.orderId()==fee.orderId())) throw unavailable();
|
||||
result.putIfAbsent(fee.orderId()+":"+fee.mainStatus(),fee);
|
||||
}
|
||||
var sources=facts.stream().filter(WorkflowFacts.Fact::departed).map(f->new WorkflowFacts.Source(f.tenantId(),f.inboundId())).distinct().toList();
|
||||
var sources=facts.stream().filter(f->f.departed() || f.clearanceUploaded()).map(f->new WorkflowFacts.Source(f.tenantId(),f.inboundId())).distinct().toList();
|
||||
Map<String,WorkflowFacts.Overseas> overseas=new HashMap<>();
|
||||
if(!sources.isEmpty()) {
|
||||
try {
|
||||
@@ -56,16 +56,18 @@ public class WorkflowTabService {
|
||||
Set<String> keys=new LinkedHashSet<>();
|
||||
if("MEASURE_PENDING".equals(process) && "PENDING".equals(f.reviewStatus())) keys.add("MEASUREMENT_REVIEW");
|
||||
boolean signed="DELIVERED".equals(process);
|
||||
// 报关页签按资料与实装事实汇总,不依赖点击开始报关;实际开船后转入清关链路。
|
||||
if(!f.departed() && !signed) {
|
||||
// 报关完成即进入待清关;不再等待船期接口补齐实际离港节点。
|
||||
boolean clearanceStage=f.customsCompleted() || f.departed() || f.clearanceUploaded();
|
||||
if(!clearanceStage && !signed) {
|
||||
if(f.missingCustoms()) keys.add("WAIT_CUSTOMS_DOCS");
|
||||
if(f.exportCustomsReady()) keys.add("EXPORT_CUSTOMS");
|
||||
}
|
||||
if(signed) keys.add("SIGNED");
|
||||
if(f.departed() && !signed) {
|
||||
if(clearanceStage && !signed) {
|
||||
if(!f.clearanceUploaded()) keys.add("SAILING_CLEARANCE");
|
||||
if(o!=null) {
|
||||
if(f.clearanceUploaded() && !o.unloaded()) keys.add("WAIT_UNLOAD");
|
||||
// 清关资料保存成功后才进入海外待办;出库事实优先于拆柜和排车待办。
|
||||
if(f.clearanceUploaded() && o!=null) {
|
||||
if(!o.outboundExists() && !o.unloaded()) keys.add("WAIT_UNLOAD");
|
||||
// 取消的出库不构成有效覆盖,读取事实后自动回显,不能保存永久隐藏标志。
|
||||
if(!o.outboundExists()) keys.add("WAIT_DISPATCH");
|
||||
if(o.pendingOutbound()) keys.add("OUTBOUND_DOCUMENT");
|
||||
|
||||
@@ -392,7 +392,7 @@ paths:
|
||||
customsOrderIds: {type: array, items: {type: integer, format: int64}}
|
||||
progress:
|
||||
type: array
|
||||
description: 实际装载片段投影;报关要求COMPLETED,目的港清关要求PICKUP_READY实际时间,缺失节点不推测。
|
||||
description: 实际装载片段投影;报关要求COMPLETED,目的港清关以本柜DESTINATION_CLEARANCE资料成功绑定file_id为准,不等待PICKUP_READY船节点。
|
||||
items:
|
||||
type: object
|
||||
required: [orderId, stage, partial]
|
||||
@@ -1674,7 +1674,7 @@ components:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, WAIT_SPLIT, LOADING, LOADED, DELIVERED, WAIT_WEIGHT_CONFIRM, WEIGHT_CONFIRMED, WAIT_CUSTOMS_DOCS, EXPORT_CUSTOMS, SAILING_CLEARANCE, WAIT_UNLOAD, WAIT_DISPATCH, OUTBOUND_DOCUMENT, DELIVERING, SIGNED, MEASUREMENT_REVIEW]
|
||||
nullable: true
|
||||
description: WAIT_SPLIT 为唯一待分柜筛选值;WAIT_CUSTOMS_DOCS 为实际装柜后缺少本票客户报关资料;EXPORT_CUSTOMS 按本票资料已上传且实际装柜完成汇总,买单无需客户退税资料,不依赖点击开始报关,实际开船后移出;SAILING_CLEARANCE 按手动或拉取的实际离港节点进入,清关资料上传后进入 WAIT_UNLOAD,实际卸柜完成后移出。
|
||||
description: WAIT_SPLIT 为唯一待分柜筛选值;WAIT_CUSTOMS_DOCS 为实际装柜后缺少本票客户报关资料;EXPORT_CUSTOMS 按本票资料已上传且实际装柜完成汇总,买单无需客户退税资料,不依赖点击开始报关,报关完成或实际开船后移出;SAILING_CLEARANCE 在整柜报关完成后进入,也接受实际离港事实;清关资料保存成功后才进入 WAIT_UNLOAD/WAIT_DISPATCH。WAIT_UNLOAD 持续到实际卸柜或生成有效出库单;生成出库单后进入 OUTBOUND_DOCUMENT,实际出库后进入 DELIVERING,派送完成进入 SIGNED。
|
||||
customerId: {type: integer, format: int64, nullable: true}
|
||||
receivingWarehouseId: {type: integer, format: int64, nullable: true}
|
||||
destinationWarehouseId: {type: integer, format: int64, nullable: true}
|
||||
|
||||
+7
@@ -30,6 +30,7 @@ class MerchantWorkbenchFilterMapperTest {
|
||||
jdbc.execute("ALTER TABLE fl_loading_plan_item ADD id BIGINT GENERATED BY DEFAULT AS IDENTITY");
|
||||
jdbc.execute("CREATE TABLE fl_loading_pick_task(id BIGINT,loading_plan_id BIGINT,task_status VARCHAR(32))");
|
||||
jdbc.execute("CREATE TABLE fl_loading_pick_item(pick_task_id BIGINT,loading_plan_item_id BIGINT,item_status VARCHAR(32),picked_package_count INT)");
|
||||
jdbc.execute("CREATE TABLE fl_container_document(container_booking_id BIGINT,document_type VARCHAR(40),file_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE fl_carrier_schedule_node_fact(container_booking_id BIGINT,standard_node VARCHAR(64),actual_at TIMESTAMP)");
|
||||
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.addMapper(MerchantWorkbenchFilterMapper.class);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);
|
||||
@@ -95,6 +96,12 @@ class MerchantWorkbenchFilterMapperTest {
|
||||
jdbc.update("INSERT INTO fl_carrier_schedule_node_fact VALUES(100,'PICKUP_READY',NULL)");
|
||||
assertThat(mapper.progressFragments(11, null)).allMatch(f->f.stage()==2);
|
||||
jdbc.update("UPDATE fl_carrier_schedule_node_fact SET actual_at=CURRENT_TIMESTAMP");
|
||||
assertThat(mapper.progressFragments(11, null)).allMatch(f->f.stage()==2);
|
||||
// 其他柜、其他资料类型或未绑定文件不能视为清关资料提交成功。
|
||||
jdbc.update("INSERT INTO fl_container_document VALUES(999,'DESTINATION_CLEARANCE',1),(100,'DELIVERY_ORDER',2),(100,'DESTINATION_CLEARANCE',NULL)");
|
||||
assertThat(mapper.progressFragments(11, null)).allMatch(f->f.stage()==2);
|
||||
jdbc.update("UPDATE fl_carrier_schedule_node_fact SET actual_at=NULL");
|
||||
jdbc.update("UPDATE fl_container_document SET file_id=3 WHERE document_type='DESTINATION_CLEARANCE' AND container_booking_id=100");
|
||||
assertThat(mapper.progressFragments(11, null)).allMatch(f->f.stage()==3);
|
||||
assertThat(mapper.progressFragments(11,List.of(101L))).singleElement().satisfies(f->{assertThat(f.orderId()).isEqualTo(101);assertThat(f.stage()).isEqualTo(3);});
|
||||
jdbc.update("UPDATE fl_container_booking SET record_status='CANCELLED'");
|
||||
|
||||
@@ -13,7 +13,7 @@ import static org.mockito.Mockito.*;
|
||||
class WorkflowBoundaryTest {
|
||||
/** 真实开船来源的测试事实。 */
|
||||
private WorkflowFacts.Fact fact() {
|
||||
return new WorkflowFacts.Fact(11,1,101,"APPROVED",true,false,false,false);
|
||||
return new WorkflowFacts.Fact(11,1,101,"APPROVED",true,false,false,false,false);
|
||||
}
|
||||
|
||||
/** 下游漏来源或返回其他客户均整体失败。 */
|
||||
@@ -33,4 +33,23 @@ class WorkflowBoundaryTest {
|
||||
assertThatThrownBy(()->service.rows(List.of(inbound))).isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
/** 未记录离港时,报关完成即可待清关;上传清关资料后也必须查询精确海外来源。 */
|
||||
@Test void completedCustomsAndClearanceUploadDoNotWaitForDeparture() {
|
||||
var repository=mock(WorkflowFactRepository.class);
|
||||
var downstream=mock(LastlegWorkflowClient.class);
|
||||
var service=new WorkflowTabService(repository,downstream);
|
||||
var inbound=new InboundOrderEntity();inbound.setId(1L);inbound.setTenantId(11L);
|
||||
inbound.setOrderId(101L);inbound.setProcessStatus("LOADED");
|
||||
when(repository.facts(List.of(1L))).thenReturn(List.of(
|
||||
new WorkflowFacts.Fact(11,1,101,"APPROVED",false,false,false,true,true)));
|
||||
assertThat(service.rows(List.of(inbound))).extracting(r->r.mainStatus()).containsExactly("SAILING_CLEARANCE");
|
||||
verifyNoInteractions(downstream);
|
||||
when(repository.facts(List.of(1L))).thenReturn(List.of(
|
||||
new WorkflowFacts.Fact(11,1,101,"APPROVED",false,true,false,true,true)));
|
||||
when(downstream.read(any(),any())).thenReturn(ApiResponse.success(List.of(
|
||||
new WorkflowFacts.Overseas(11,1,false,false,false,false))));
|
||||
assertThat(service.rows(List.of(inbound))).extracting(r->r.mainStatus()).containsExactly("WAIT_UNLOAD","WAIT_DISPATCH");
|
||||
verify(downstream).read(any(),eq(new WorkflowFacts.Request(List.of(new WorkflowFacts.Source(11,1)))));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,33 +3,46 @@ import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
/** 取消回显、分柜隔离和资料页签回归。 */
|
||||
class WorkflowBusinessTest {
|
||||
/** 报关资料齐备进入汇总,实际开船及清关资料分别驱动后续页签。 */
|
||||
/** 报关完成进入待清关,清关资料保存后才进入拆柜与排车待办。 */
|
||||
@Test void customsDocumentsDepartureAndClearanceDriveTabs() {
|
||||
var missing=new WorkflowFacts.Fact(11,1,101,"APPROVED",false,false,true,false);
|
||||
var uploaded=new WorkflowFacts.Fact(11,1,101,"APPROVED",false,false,false,true);
|
||||
var departed=new WorkflowFacts.Fact(11,1,101,"APPROVED",true,false,false,true);
|
||||
var cleared=new WorkflowFacts.Fact(11,1,101,"APPROVED",true,true,false,true);
|
||||
var uploadedBeforeDeparture=new WorkflowFacts.Fact(11,1,101,"APPROVED",false,true,false,true);
|
||||
var missing=new WorkflowFacts.Fact(11,1,101,"APPROVED",false,false,true,false,false);
|
||||
var uploaded=new WorkflowFacts.Fact(11,1,101,"APPROVED",false,false,false,true,false);
|
||||
var departed=new WorkflowFacts.Fact(11,1,101,"APPROVED",true,false,false,true,false);
|
||||
var cleared=new WorkflowFacts.Fact(11,1,101,"APPROVED",true,true,false,true,false);
|
||||
var completed=new WorkflowFacts.Fact(11,1,101,"APPROVED",false,false,false,true,true);
|
||||
var uploadedBeforeDeparture=new WorkflowFacts.Fact(11,1,101,"APPROVED",false,true,false,true,false);
|
||||
var pendingUnload=new WorkflowFacts.Overseas(11,1,false,false,false,false);
|
||||
var unloaded=new WorkflowFacts.Overseas(11,1,false,false,false,true);
|
||||
assertThat(WorkflowTabService.classify(missing,"LOADED",null))
|
||||
.contains("WAIT_CUSTOMS_DOCS").doesNotContain("EXPORT_CUSTOMS","SAILING_CLEARANCE","WAIT_UNLOAD");
|
||||
assertThat(WorkflowTabService.classify(uploaded,"LOADED",null))
|
||||
.contains("EXPORT_CUSTOMS").doesNotContain("WAIT_CUSTOMS_DOCS","SAILING_CLEARANCE","WAIT_UNLOAD");
|
||||
assertThat(WorkflowTabService.classify(completed,"LOADED",null))
|
||||
.containsExactly("SAILING_CLEARANCE");
|
||||
assertThat(WorkflowTabService.classify(uploadedBeforeDeparture,"LOADED",pendingUnload))
|
||||
.contains("EXPORT_CUSTOMS").doesNotContain("SAILING_CLEARANCE","WAIT_UNLOAD");
|
||||
.containsExactly("WAIT_UNLOAD","WAIT_DISPATCH");
|
||||
assertThat(WorkflowTabService.classify(departed,"LOADED",pendingUnload))
|
||||
.contains("SAILING_CLEARANCE").doesNotContain("WAIT_CUSTOMS_DOCS","EXPORT_CUSTOMS","WAIT_UNLOAD");
|
||||
.containsExactly("SAILING_CLEARANCE");
|
||||
assertThat(WorkflowTabService.classify(cleared,"LOADED",pendingUnload))
|
||||
.contains("WAIT_UNLOAD").doesNotContain("WAIT_CUSTOMS_DOCS","EXPORT_CUSTOMS","SAILING_CLEARANCE");
|
||||
.containsExactly("WAIT_UNLOAD","WAIT_DISPATCH");
|
||||
assertThat(WorkflowTabService.classify(cleared,"LOADED",unloaded))
|
||||
.doesNotContain("WAIT_UNLOAD","SAILING_CLEARANCE","EXPORT_CUSTOMS");
|
||||
assertThat(WorkflowTabService.classify(uploaded,"DELIVERED",unloaded))
|
||||
.contains("SIGNED").doesNotContain("EXPORT_CUSTOMS","WAIT_CUSTOMS_DOCS");
|
||||
}
|
||||
/** 清关前不能被海外建单事实提前推进,清关后出库与派送依次替换待办。 */
|
||||
@Test void downstreamFactsCannotBypassClearanceAndOutboundReplacesQueues() {
|
||||
var pending=new WorkflowFacts.Overseas(11,1,true,true,false,false);
|
||||
var shipped=new WorkflowFacts.Overseas(11,1,true,false,true,false);
|
||||
assertThat(WorkflowTabService.classify(fact(1,false),"LOADED",pending)).containsExactly("SAILING_CLEARANCE");
|
||||
assertThat(WorkflowTabService.classify(fact(1,false),"LOADED",shipped)).containsExactly("SAILING_CLEARANCE");
|
||||
assertThat(WorkflowTabService.classify(fact(1,true),"LOADED",pending)).containsExactly("OUTBOUND_DOCUMENT");
|
||||
assertThat(WorkflowTabService.classify(fact(1,true),"LOADED",shipped)).containsExactly("DELIVERING");
|
||||
assertThat(WorkflowTabService.classify(fact(1,true),"DELIVERED",shipped)).containsExactly("SIGNED");
|
||||
}
|
||||
/** 构造独立分柜来源。 */
|
||||
private WorkflowFacts.Fact fact(long id,boolean uploaded) {
|
||||
return new WorkflowFacts.Fact(11,id,100+id,"APPROVED",true,uploaded,false,false);
|
||||
return new WorkflowFacts.Fact(11,id,100+id,"APPROVED",true,uploaded,false,false,false);
|
||||
}
|
||||
/** 有效建单移出待排车,取消后恢复;实际出库不计未出库单。 */
|
||||
@Test void cancellationRestoresDispatchQueue() {
|
||||
@@ -45,9 +58,9 @@ class WorkflowBusinessTest {
|
||||
/** 一柜资料上传不覆盖另一柜;签收不再进入待排。 */
|
||||
@Test void independentSlicesAndUploadedClearance() {
|
||||
assertThat(WorkflowTabService.classify(fact(1,false),"LOADED",new WorkflowFacts.Overseas(11,1,false,false,false,false)))
|
||||
.contains("SAILING_CLEARANCE","WAIT_DISPATCH").doesNotContain("WAIT_UNLOAD");
|
||||
.containsExactly("SAILING_CLEARANCE");
|
||||
assertThat(WorkflowTabService.classify(fact(2,true),"LOADED",new WorkflowFacts.Overseas(11,2,true,true,false,false)))
|
||||
.contains("WAIT_UNLOAD","OUTBOUND_DOCUMENT").doesNotContain("SAILING_CLEARANCE","WAIT_DISPATCH");
|
||||
.containsExactly("OUTBOUND_DOCUMENT");
|
||||
assertThat(WorkflowTabService.classify(fact(1,false),"DELIVERED",null)).contains("SIGNED").doesNotContain("WAIT_DISPATCH");
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -60,10 +60,11 @@ class WorkflowFactPersistenceTest {
|
||||
db.update("INSERT INTO fl_customs_customer_document VALUES(10,11,'ACTIVE',8)");
|
||||
db.update("INSERT INTO fl_customs_customer_document_inbound VALUES(1,11,10)");
|
||||
assertThat(mapper.facts(List.of(1L))).singleElement().satisfies(f->{assertThat(f.missingCustoms()).isFalse();assertThat(f.exportCustomsReady()).isTrue();});
|
||||
// 本票上传后即可汇总,不必等同柜其他票或点击开始报关;完成报关后仍等待实际开船。
|
||||
// 本票资料上传后进入出口报关汇总,报关完成事实独立驱动待清关。
|
||||
for (String status : List.of("READY_TO_START", "DECLARING", "COMPLETED")) {
|
||||
db.update("UPDATE fl_customs_container SET status=? WHERE id=8", status);
|
||||
assertThat(mapper.facts(List.of(1L)).get(0).exportCustomsReady()).isTrue();
|
||||
assertThat(mapper.facts(List.of(1L)).get(0).customsCompleted()).isEqualTo("COMPLETED".equals(status));
|
||||
}
|
||||
db.update("UPDATE fl_customs_customer_document SET status='DELETED' WHERE id=10");
|
||||
assertThat(mapper.facts(List.of(1L))).singleElement().satisfies(f->{
|
||||
|
||||
Reference in New Issue
Block a user